logo

pleroma

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

fetcher_test.exs (26613B)


  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.Object
  8. alias Pleroma.Object.Fetcher
  9. alias Pleroma.Web.ActivityPub.ObjectValidator
  10. require Pleroma.Constants
  11. import Mock
  12. import Pleroma.Factory
  13. import Tesla.Mock
  14. setup do
  15. mock(fn
  16. %{method: :get, url: "https://mastodon.example.org/users/userisgone"} ->
  17. %Tesla.Env{status: 410}
  18. %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} ->
  19. %Tesla.Env{status: 404}
  20. %{
  21. method: :get,
  22. url:
  23. "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
  24. } ->
  25. %Tesla.Env{
  26. status: 200,
  27. headers: [{"content-type", "application/json"}],
  28. body: File.read!("test/fixtures/spoofed-object.json")
  29. }
  30. env ->
  31. apply(HttpRequestMock, :request, [env])
  32. end)
  33. :ok
  34. end
  35. describe "error cases" do
  36. setup do
  37. mock(fn
  38. %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
  39. %Tesla.Env{
  40. status: 200,
  41. body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"),
  42. headers: HttpRequestMock.activitypub_object_headers()
  43. }
  44. %{method: :get, url: "https://social.sakamoto.gq/users/eal"} ->
  45. %Tesla.Env{
  46. status: 200,
  47. body: File.read!("test/fixtures/fetch_mocks/eal.json"),
  48. headers: HttpRequestMock.activitypub_object_headers()
  49. }
  50. %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} ->
  51. %Tesla.Env{
  52. status: 200,
  53. body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json"),
  54. headers: HttpRequestMock.activitypub_object_headers()
  55. }
  56. %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} ->
  57. %Tesla.Env{
  58. status: 500
  59. }
  60. %{
  61. method: :get,
  62. url: "https://stereophonic.space/objects/02997b83-3ea7-4b63-94af-ef3aa2d4ed17"
  63. } ->
  64. %Tesla.Env{
  65. status: 500
  66. }
  67. end)
  68. :ok
  69. end
  70. test "it works when fetching the OP actor errors out" do
  71. # Here we simulate a case where the author of the OP can't be read
  72. assert {:ok, _} =
  73. Fetcher.fetch_object_from_id(
  74. "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"
  75. )
  76. end
  77. end
  78. describe "max thread distance restriction" do
  79. @ap_id "http://mastodon.example.org/@admin/99541947525187367"
  80. setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
  81. test "it returns thread depth exceeded error if thread depth is exceeded" do
  82. clear_config([:instance, :federation_incoming_replies_max_depth], 0)
  83. assert {:allowed_depth, false} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
  84. end
  85. test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
  86. clear_config([:instance, :federation_incoming_replies_max_depth], 0)
  87. assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id)
  88. end
  89. test "it fetches object if requested depth does not exceed max thread depth" do
  90. clear_config([:instance, :federation_incoming_replies_max_depth], 10)
  91. assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10)
  92. end
  93. end
  94. describe "actor origin containment" do
  95. test "it rejects objects with a bogus origin" do
  96. {:containment, :error} =
  97. 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. {:containment, :error} =
  101. Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
  102. end
  103. test "it rejects objects when attributedTo is wrong (variant 2)" do
  104. {:containment, :error} =
  105. Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
  106. end
  107. end
  108. describe "fetching an object" do
  109. test "it fetches an object" do
  110. {:ok, object} =
  111. Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
  112. assert _activity = Activity.get_create_by_object_ap_id(object.data["id"])
  113. {:ok, object_again} =
  114. Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
  115. assert [attachment] = object.data["attachment"]
  116. assert is_list(attachment["url"])
  117. assert object == object_again
  118. end
  119. test "Return MRF reason when fetched status is rejected by one" do
  120. clear_config([:mrf_keyword, :reject], ["yeah"])
  121. clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
  122. assert {:transmogrifier, {:reject, "[KeywordPolicy] Matches with rejected keyword"}} ==
  123. Fetcher.fetch_object_from_id(
  124. "http://mastodon.example.org/@admin/99541947525187367"
  125. )
  126. end
  127. test "it does not fetch a spoofed object uploaded on an instance as an attachment" do
  128. assert {:fetch, {:error, {:content_type, "application/json"}}} =
  129. Fetcher.fetch_object_from_id(
  130. "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
  131. )
  132. end
  133. test "it does not fetch from local instance" do
  134. local_url = Pleroma.Web.Endpoint.url() <> "/objects/local_resource"
  135. assert {:fetch, {:error, "Trying to fetch local resource"}} =
  136. Fetcher.fetch_object_from_id(local_url)
  137. end
  138. test "it validates content-type headers according to ActivityPub spec" do
  139. # Setup a mock for an object with invalid content-type
  140. mock(fn
  141. %{method: :get, url: "https://example.com/objects/invalid-content-type"} ->
  142. %Tesla.Env{
  143. status: 200,
  144. # Not a valid AP content-type
  145. headers: [{"content-type", "application/json"}],
  146. body:
  147. Jason.encode!(%{
  148. "id" => "https://example.com/objects/invalid-content-type",
  149. "type" => "Note",
  150. "content" => "This has an invalid content type",
  151. "actor" => "https://example.com/users/actor",
  152. "attributedTo" => "https://example.com/users/actor"
  153. })
  154. }
  155. end)
  156. assert {:fetch, {:error, {:content_type, "application/json"}}} =
  157. Fetcher.fetch_object_from_id("https://example.com/objects/invalid-content-type")
  158. end
  159. test "it accepts objects with application/ld+json and ActivityStreams profile" do
  160. # Setup a mock for an object with ld+json content-type and AS profile
  161. mock(fn
  162. %{method: :get, url: "https://example.com/objects/valid-ld-json"} ->
  163. %Tesla.Env{
  164. status: 200,
  165. headers: [
  166. {"content-type",
  167. "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}
  168. ],
  169. body:
  170. Jason.encode!(%{
  171. "id" => "https://example.com/objects/valid-ld-json",
  172. "type" => "Note",
  173. "content" => "This has a valid ld+json content type",
  174. "actor" => "https://example.com/users/actor",
  175. "attributedTo" => "https://example.com/users/actor"
  176. })
  177. }
  178. end)
  179. # This should pass if content-type validation works correctly
  180. assert {:ok, object} =
  181. Fetcher.fetch_and_contain_remote_object_from_id(
  182. "https://example.com/objects/valid-ld-json"
  183. )
  184. assert object["content"] == "This has a valid ld+json content type"
  185. end
  186. test "it rejects objects with no content-type header" do
  187. # Setup a mock for an object with no content-type header
  188. mock(fn
  189. %{method: :get, url: "https://example.com/objects/no-content-type"} ->
  190. %Tesla.Env{
  191. status: 200,
  192. # No content-type header
  193. headers: [],
  194. body:
  195. Jason.encode!(%{
  196. "id" => "https://example.com/objects/no-content-type",
  197. "type" => "Note",
  198. "content" => "This has no content type header",
  199. "actor" => "https://example.com/users/actor",
  200. "attributedTo" => "https://example.com/users/actor"
  201. })
  202. }
  203. end)
  204. # We want to test that the request fails with a missing content-type error
  205. # but the actual error is {:fetch, {:error, nil}} - we'll check for this format
  206. result = Fetcher.fetch_object_from_id("https://example.com/objects/no-content-type")
  207. assert {:fetch, {:error, nil}} = result
  208. end
  209. end
  210. describe "implementation quirks" do
  211. test "it can fetch plume articles" do
  212. {:ok, object} =
  213. Fetcher.fetch_object_from_id(
  214. "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
  215. )
  216. assert object
  217. end
  218. test "it can fetch peertube videos" do
  219. {:ok, object} =
  220. Fetcher.fetch_object_from_id(
  221. "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
  222. )
  223. assert object
  224. end
  225. test "it can fetch Mobilizon events" do
  226. {:ok, object} =
  227. Fetcher.fetch_object_from_id(
  228. "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
  229. )
  230. assert object
  231. end
  232. test "it can fetch Honk events" do
  233. {:ok, object} =
  234. Fetcher.fetch_object_from_id("https://honk.tedunangst.com/u/tedu/h/8dkPX284T8286Mm9HD")
  235. assert object
  236. end
  237. test "it can fetch wedistribute articles" do
  238. {:ok, object} =
  239. Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
  240. assert object
  241. end
  242. test "all objects with fake directions are rejected by the object fetcher" do
  243. assert {:error, _} =
  244. Fetcher.fetch_and_contain_remote_object_from_id(
  245. "https://info.pleroma.site/activity4.json"
  246. )
  247. end
  248. test "handle HTTP 410 Gone response" do
  249. assert {:error, :not_found} ==
  250. Fetcher.fetch_and_contain_remote_object_from_id(
  251. "https://mastodon.example.org/users/userisgone"
  252. )
  253. end
  254. test "handle HTTP 404 response" do
  255. assert {:error, :not_found} ==
  256. Fetcher.fetch_and_contain_remote_object_from_id(
  257. "https://mastodon.example.org/users/userisgone404"
  258. )
  259. end
  260. test "it can fetch pleroma polls with attachments" do
  261. {:ok, object} =
  262. Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
  263. assert object
  264. end
  265. end
  266. describe "pruning" do
  267. test "it can refetch pruned objects" do
  268. object_id = "http://mastodon.example.org/@admin/99541947525187367"
  269. {:ok, object} = Fetcher.fetch_object_from_id(object_id)
  270. assert object
  271. {:ok, _object} = Object.prune(object)
  272. refute Object.get_by_ap_id(object_id)
  273. {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
  274. assert object.data["id"] == object_two.data["id"]
  275. assert object.id != object_two.id
  276. end
  277. end
  278. describe "signed fetches" do
  279. setup do: clear_config([:activitypub, :sign_object_fetches])
  280. test_with_mock "it signs fetches when configured to do so",
  281. Pleroma.Signature,
  282. [:passthrough],
  283. [] do
  284. clear_config([:activitypub, :sign_object_fetches], true)
  285. Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
  286. assert called(Pleroma.Signature.sign(:_, :_))
  287. end
  288. test_with_mock "it doesn't sign fetches when not configured to do so",
  289. Pleroma.Signature,
  290. [:passthrough],
  291. [] do
  292. clear_config([:activitypub, :sign_object_fetches], false)
  293. Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
  294. refute called(Pleroma.Signature.sign(:_, :_))
  295. end
  296. end
  297. describe "refetching" do
  298. setup do
  299. insert(:user, ap_id: "https://mastodon.social/users/emelie")
  300. object1 = %{
  301. "id" => "https://mastodon.social/1",
  302. "actor" => "https://mastodon.social/users/emelie",
  303. "attributedTo" => "https://mastodon.social/users/emelie",
  304. "type" => "Note",
  305. "content" => "test 1",
  306. "bcc" => [],
  307. "bto" => [],
  308. "cc" => [],
  309. "to" => [Pleroma.Constants.as_public()],
  310. "summary" => "",
  311. "published" => "2023-05-08 23:43:20Z",
  312. "updated" => "2023-05-09 23:43:20Z"
  313. }
  314. {:ok, local_object1, _} = ObjectValidator.validate(object1, [])
  315. object2 = %{
  316. "id" => "https://mastodon.social/2",
  317. "actor" => "https://mastodon.social/users/emelie",
  318. "attributedTo" => "https://mastodon.social/users/emelie",
  319. "type" => "Note",
  320. "content" => "test 2",
  321. "bcc" => [],
  322. "bto" => [],
  323. "cc" => [],
  324. "to" => [Pleroma.Constants.as_public()],
  325. "summary" => "",
  326. "published" => "2023-05-08 23:43:20Z",
  327. "updated" => "2023-05-09 23:43:25Z",
  328. "formerRepresentations" => %{
  329. "type" => "OrderedCollection",
  330. "orderedItems" => [
  331. %{
  332. "type" => "Note",
  333. "content" => "orig 2",
  334. "actor" => "https://mastodon.social/users/emelie",
  335. "attributedTo" => "https://mastodon.social/users/emelie",
  336. "bcc" => [],
  337. "bto" => [],
  338. "cc" => [],
  339. "to" => [Pleroma.Constants.as_public()],
  340. "summary" => "",
  341. "published" => "2023-05-08 23:43:20Z",
  342. "updated" => "2023-05-09 23:43:21Z"
  343. }
  344. ],
  345. "totalItems" => 1
  346. }
  347. }
  348. {:ok, local_object2, _} = ObjectValidator.validate(object2, [])
  349. mock(fn
  350. %{
  351. method: :get,
  352. url: "https://mastodon.social/1"
  353. } ->
  354. %Tesla.Env{
  355. status: 200,
  356. headers: [{"content-type", "application/activity+json"}],
  357. body: Jason.encode!(object1 |> Map.put("updated", "2023-05-09 23:44:20Z"))
  358. }
  359. %{
  360. method: :get,
  361. url: "https://mastodon.social/2"
  362. } ->
  363. %Tesla.Env{
  364. status: 200,
  365. headers: [{"content-type", "application/activity+json"}],
  366. body: Jason.encode!(object2 |> Map.put("updated", "2023-05-09 23:44:20Z"))
  367. }
  368. %{
  369. method: :get,
  370. url: "https://mastodon.social/users/emelie/collections/featured"
  371. } ->
  372. %Tesla.Env{
  373. status: 200,
  374. headers: [{"content-type", "application/activity+json"}],
  375. body:
  376. Jason.encode!(%{
  377. "id" => "https://mastodon.social/users/emelie/collections/featured",
  378. "type" => "OrderedCollection",
  379. "actor" => "https://mastodon.social/users/emelie",
  380. "attributedTo" => "https://mastodon.social/users/emelie",
  381. "orderedItems" => [],
  382. "totalItems" => 0
  383. })
  384. }
  385. env ->
  386. apply(HttpRequestMock, :request, [env])
  387. end)
  388. %{object1: local_object1, object2: local_object2}
  389. end
  390. test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
  391. full_object1 =
  392. object1
  393. |> Map.merge(%{
  394. "formerRepresentations" => %{
  395. "type" => "OrderedCollection",
  396. "orderedItems" => [
  397. %{
  398. "type" => "Note",
  399. "content" => "orig 2",
  400. "actor" => "https://mastodon.social/users/emelie",
  401. "attributedTo" => "https://mastodon.social/users/emelie",
  402. "bcc" => [],
  403. "bto" => [],
  404. "cc" => [],
  405. "to" => [Pleroma.Constants.as_public()],
  406. "summary" => "",
  407. "published" => "2023-05-08 23:43:20Z"
  408. }
  409. ],
  410. "totalItems" => 1
  411. }
  412. })
  413. {:ok, o} = Object.create(full_object1)
  414. assert {:ok, refetched} = Fetcher.refetch_object(o)
  415. assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
  416. refetched.data
  417. end
  418. test "it uses formerRepresentations from remote if possible", %{object2: object2} do
  419. {:ok, o} = Object.create(object2)
  420. assert {:ok, refetched} = Fetcher.refetch_object(o)
  421. assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
  422. refetched.data
  423. end
  424. test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
  425. full_object2 =
  426. object2
  427. |> Map.merge(%{
  428. "content" => "mew mew #def",
  429. "formerRepresentations" => %{
  430. "type" => "OrderedCollection",
  431. "orderedItems" => [
  432. %{"type" => "Note", "content" => "mew mew 2"}
  433. ],
  434. "totalItems" => 1
  435. }
  436. })
  437. {:ok, o} = Object.create(full_object2)
  438. assert {:ok, refetched} = Fetcher.refetch_object(o)
  439. assert %{
  440. "content" => "test 2",
  441. "formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
  442. } = refetched.data
  443. end
  444. test "it adds to formerRepresentations if the remote does not have one and the object has changed",
  445. %{object1: object1} do
  446. full_object1 =
  447. object1
  448. |> Map.merge(%{
  449. "content" => "mew mew #def",
  450. "formerRepresentations" => %{
  451. "type" => "OrderedCollection",
  452. "orderedItems" => [
  453. %{"type" => "Note", "content" => "mew mew 1"}
  454. ],
  455. "totalItems" => 1
  456. }
  457. })
  458. {:ok, o} = Object.create(full_object1)
  459. assert {:ok, refetched} = Fetcher.refetch_object(o)
  460. assert %{
  461. "content" => "test 1",
  462. "formerRepresentations" => %{
  463. "orderedItems" => [
  464. %{"content" => "mew mew #def"},
  465. %{"content" => "mew mew 1"}
  466. ],
  467. "totalItems" => 2
  468. }
  469. } = refetched.data
  470. end
  471. test "it keeps the history intact if only updated time has changed",
  472. %{object1: object1} do
  473. full_object1 =
  474. object1
  475. |> Map.merge(%{
  476. "updated" => "2023-05-08 23:43:47Z",
  477. "formerRepresentations" => %{
  478. "type" => "OrderedCollection",
  479. "orderedItems" => [
  480. %{"type" => "Note", "content" => "mew mew 1"}
  481. ],
  482. "totalItems" => 1
  483. }
  484. })
  485. {:ok, o} = Object.create(full_object1)
  486. assert {:ok, refetched} = Fetcher.refetch_object(o)
  487. assert %{
  488. "content" => "test 1",
  489. "formerRepresentations" => %{
  490. "orderedItems" => [
  491. %{"content" => "mew mew 1"}
  492. ],
  493. "totalItems" => 1
  494. }
  495. } = refetched.data
  496. end
  497. test "it goes through ObjectValidator and MRF", %{object2: object2} do
  498. with_mock Pleroma.Web.ActivityPub.MRF, [:passthrough],
  499. filter: fn
  500. %{"type" => "Note"} = object ->
  501. {:ok, Map.put(object, "content", "MRFd content")}
  502. arg ->
  503. passthrough([arg])
  504. end do
  505. {:ok, o} = Object.create(object2)
  506. assert {:ok, refetched} = Fetcher.refetch_object(o)
  507. assert %{"content" => "MRFd content"} = refetched.data
  508. end
  509. end
  510. end
  511. describe "cross-domain redirect handling" do
  512. setup do
  513. mock(fn
  514. # Cross-domain redirect with original domain in id
  515. %{method: :get, url: "https://original.test/objects/123"} ->
  516. %Tesla.Env{
  517. status: 200,
  518. url: "https://media.test/objects/123",
  519. headers: [{"content-type", "application/activity+json"}],
  520. body:
  521. Jason.encode!(%{
  522. "id" => "https://original.test/objects/123",
  523. "type" => "Note",
  524. "content" => "This is redirected content",
  525. "actor" => "https://original.test/users/actor",
  526. "attributedTo" => "https://original.test/users/actor"
  527. })
  528. }
  529. # Cross-domain redirect with final domain in id
  530. %{method: :get, url: "https://original.test/objects/final-domain-id"} ->
  531. %Tesla.Env{
  532. status: 200,
  533. url: "https://media.test/objects/final-domain-id",
  534. headers: [{"content-type", "application/activity+json"}],
  535. body:
  536. Jason.encode!(%{
  537. "id" => "https://media.test/objects/final-domain-id",
  538. "type" => "Note",
  539. "content" => "This has final domain in id",
  540. "actor" => "https://original.test/users/actor",
  541. "attributedTo" => "https://original.test/users/actor"
  542. })
  543. }
  544. # No redirect - same domain
  545. %{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
  546. %Tesla.Env{
  547. status: 200,
  548. url: "https://original.test/objects/different-path",
  549. headers: [{"content-type", "application/activity+json"}],
  550. body:
  551. Jason.encode!(%{
  552. "id" => "https://original.test/objects/same-domain-redirect",
  553. "type" => "Note",
  554. "content" => "This has a same-domain redirect",
  555. "actor" => "https://original.test/users/actor",
  556. "attributedTo" => "https://original.test/users/actor"
  557. })
  558. }
  559. # Test case with missing url field in response (common in tests)
  560. %{method: :get, url: "https://original.test/objects/missing-url"} ->
  561. %Tesla.Env{
  562. status: 200,
  563. # No url field
  564. headers: [{"content-type", "application/activity+json"}],
  565. body:
  566. Jason.encode!(%{
  567. "id" => "https://original.test/objects/missing-url",
  568. "type" => "Note",
  569. "content" => "This has no URL field in response",
  570. "actor" => "https://original.test/users/actor",
  571. "attributedTo" => "https://original.test/users/actor"
  572. })
  573. }
  574. end)
  575. :ok
  576. end
  577. test "it rejects objects from cross-domain redirects with original domain in id" do
  578. assert {:error, {:cross_domain_redirect, true}} =
  579. Fetcher.fetch_and_contain_remote_object_from_id(
  580. "https://original.test/objects/123"
  581. )
  582. end
  583. test "it rejects objects from cross-domain redirects with final domain in id" do
  584. assert {:error, {:cross_domain_redirect, true}} =
  585. Fetcher.fetch_and_contain_remote_object_from_id(
  586. "https://original.test/objects/final-domain-id"
  587. )
  588. end
  589. test "it accepts objects with same-domain redirects" do
  590. assert {:ok, data} =
  591. Fetcher.fetch_and_contain_remote_object_from_id(
  592. "https://original.test/objects/same-domain-redirect"
  593. )
  594. assert data["content"] == "This has a same-domain redirect"
  595. end
  596. test "it handles responses without URL field (common in tests)" do
  597. assert {:ok, data} =
  598. Fetcher.fetch_and_contain_remote_object_from_id(
  599. "https://original.test/objects/missing-url"
  600. )
  601. assert data["content"] == "This has no URL field in response"
  602. end
  603. end
  604. describe "fetch with history" do
  605. setup do
  606. object2 = %{
  607. "id" => "https://mastodon.social/2",
  608. "actor" => "https://mastodon.social/users/emelie",
  609. "attributedTo" => "https://mastodon.social/users/emelie",
  610. "type" => "Note",
  611. "content" => "test 2",
  612. "bcc" => [],
  613. "bto" => [],
  614. "cc" => ["https://mastodon.social/users/emelie/followers"],
  615. "to" => [],
  616. "summary" => "",
  617. "formerRepresentations" => %{
  618. "type" => "OrderedCollection",
  619. "orderedItems" => [
  620. %{
  621. "type" => "Note",
  622. "content" => "orig 2",
  623. "actor" => "https://mastodon.social/users/emelie",
  624. "attributedTo" => "https://mastodon.social/users/emelie",
  625. "bcc" => [],
  626. "bto" => [],
  627. "cc" => ["https://mastodon.social/users/emelie/followers"],
  628. "to" => [],
  629. "summary" => ""
  630. }
  631. ],
  632. "totalItems" => 1
  633. }
  634. }
  635. mock(fn
  636. %{
  637. method: :get,
  638. url: "https://mastodon.social/2"
  639. } ->
  640. %Tesla.Env{
  641. status: 200,
  642. headers: [{"content-type", "application/activity+json"}],
  643. body: Jason.encode!(object2)
  644. }
  645. %{
  646. method: :get,
  647. url: "https://mastodon.social/users/emelie/collections/featured"
  648. } ->
  649. %Tesla.Env{
  650. status: 200,
  651. headers: [{"content-type", "application/activity+json"}],
  652. body:
  653. Jason.encode!(%{
  654. "id" => "https://mastodon.social/users/emelie/collections/featured",
  655. "type" => "OrderedCollection",
  656. "actor" => "https://mastodon.social/users/emelie",
  657. "attributedTo" => "https://mastodon.social/users/emelie",
  658. "orderedItems" => [],
  659. "totalItems" => 0
  660. })
  661. }
  662. env ->
  663. apply(HttpRequestMock, :request, [env])
  664. end)
  665. %{object2: object2}
  666. end
  667. test "it gets history", %{object2: object2} do
  668. {:ok, object} = Fetcher.fetch_object_from_id(object2["id"])
  669. assert %{
  670. "formerRepresentations" => %{
  671. "type" => "OrderedCollection",
  672. "orderedItems" => [%{}]
  673. }
  674. } = object.data
  675. end
  676. end
  677. end