logo

pleroma

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

object_test.exs (15916B)


  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.ObjectTest do
  5. use Pleroma.DataCase
  6. use Oban.Testing, repo: Pleroma.Repo
  7. import ExUnit.CaptureLog
  8. import Mox
  9. import Pleroma.Factory
  10. import Tesla.Mock
  11. alias Pleroma.Activity
  12. alias Pleroma.Hashtag
  13. alias Pleroma.Object
  14. alias Pleroma.Repo
  15. alias Pleroma.Tests.ObanHelpers
  16. alias Pleroma.UnstubbedConfigMock, as: ConfigMock
  17. alias Pleroma.Web.CommonAPI
  18. setup do
  19. mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
  20. ConfigMock |> stub_with(Pleroma.Test.StaticConfig)
  21. :ok
  22. end
  23. test "returns an object by it's AP id" do
  24. object = insert(:note)
  25. found_object = Object.get_by_ap_id(object.data["id"])
  26. assert object == found_object
  27. end
  28. describe "generic changeset" do
  29. test "it ensures uniqueness of the id" do
  30. object = insert(:note)
  31. cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}})
  32. assert cs.valid?
  33. {:error, _result} = Repo.insert(cs)
  34. end
  35. end
  36. describe "deletion function" do
  37. test "deletes an object" do
  38. object = insert(:note)
  39. found_object = Object.get_by_ap_id(object.data["id"])
  40. assert object == found_object
  41. Object.delete(found_object)
  42. found_object = Object.get_by_ap_id(object.data["id"])
  43. refute object == found_object
  44. assert found_object.data["type"] == "Tombstone"
  45. end
  46. test "ensures cache is cleared for the object" do
  47. object = insert(:note)
  48. cached_object = Object.get_cached_by_ap_id(object.data["id"])
  49. assert object == cached_object
  50. Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe")
  51. Object.delete(cached_object)
  52. {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}")
  53. {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path)
  54. cached_object = Object.get_cached_by_ap_id(object.data["id"])
  55. refute object == cached_object
  56. assert cached_object.data["type"] == "Tombstone"
  57. end
  58. end
  59. describe "delete attachments" do
  60. setup do: clear_config([Pleroma.Upload])
  61. setup do: clear_config([:instance, :cleanup_attachments])
  62. test "Disabled via config" do
  63. clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
  64. clear_config([:instance, :cleanup_attachments], false)
  65. file = %Plug.Upload{
  66. content_type: "image/jpeg",
  67. path: Path.absname("test/fixtures/image.jpg"),
  68. filename: "an_image.jpg"
  69. }
  70. user = insert(:user)
  71. {:ok, %Object{} = attachment} =
  72. Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
  73. %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
  74. note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
  75. uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
  76. path = href |> Path.dirname() |> Path.basename()
  77. assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
  78. Object.delete(note)
  79. ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
  80. assert Object.get_by_id(note.id).data["deleted"]
  81. refute Object.get_by_id(attachment.id) == nil
  82. assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
  83. end
  84. test "in subdirectories" do
  85. clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
  86. clear_config([:instance, :cleanup_attachments], true)
  87. file = %Plug.Upload{
  88. content_type: "image/jpeg",
  89. path: Path.absname("test/fixtures/image.jpg"),
  90. filename: "an_image.jpg"
  91. }
  92. user = insert(:user)
  93. {:ok, %Object{} = attachment} =
  94. Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
  95. %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
  96. note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
  97. uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
  98. path = href |> Path.dirname() |> Path.basename()
  99. assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
  100. Object.delete(note)
  101. ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
  102. assert Object.get_by_id(note.id).data["deleted"]
  103. assert Object.get_by_id(attachment.id) == nil
  104. assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
  105. end
  106. test "with dedupe enabled" do
  107. clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
  108. clear_config([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe])
  109. clear_config([:instance, :cleanup_attachments], true)
  110. uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
  111. File.mkdir_p!(uploads_dir)
  112. file = %Plug.Upload{
  113. content_type: "image/jpeg",
  114. path: Path.absname("test/fixtures/image.jpg"),
  115. filename: "an_image.jpg"
  116. }
  117. user = insert(:user)
  118. {:ok, %Object{} = attachment} =
  119. Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
  120. %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
  121. note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
  122. filename = Path.basename(href)
  123. assert {:ok, files} = File.ls(uploads_dir)
  124. assert filename in files
  125. Object.delete(note)
  126. ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
  127. assert Object.get_by_id(note.id).data["deleted"]
  128. assert Object.get_by_id(attachment.id) == nil
  129. assert {:ok, files} = File.ls(uploads_dir)
  130. refute filename in files
  131. end
  132. test "with objects that have legacy data.url attribute" do
  133. clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
  134. clear_config([:instance, :cleanup_attachments], true)
  135. file = %Plug.Upload{
  136. content_type: "image/jpeg",
  137. path: Path.absname("test/fixtures/image.jpg"),
  138. filename: "an_image.jpg"
  139. }
  140. user = insert(:user)
  141. {:ok, %Object{} = attachment} =
  142. Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
  143. {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id})
  144. %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
  145. note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
  146. uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
  147. path = href |> Path.dirname() |> Path.basename()
  148. assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
  149. Object.delete(note)
  150. ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
  151. assert Object.get_by_id(note.id).data["deleted"]
  152. assert Object.get_by_id(attachment.id) == nil
  153. assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
  154. end
  155. test "With custom base_url" do
  156. clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
  157. clear_config([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/")
  158. clear_config([:instance, :cleanup_attachments], true)
  159. file = %Plug.Upload{
  160. content_type: "image/jpeg",
  161. path: Path.absname("test/fixtures/image.jpg"),
  162. filename: "an_image.jpg"
  163. }
  164. user = insert(:user)
  165. {:ok, %Object{} = attachment} =
  166. Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
  167. %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
  168. note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
  169. uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
  170. path = href |> Path.dirname() |> Path.basename()
  171. assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
  172. Object.delete(note)
  173. ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
  174. assert Object.get_by_id(note.id).data["deleted"]
  175. assert Object.get_by_id(attachment.id) == nil
  176. assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
  177. end
  178. end
  179. describe "normalizer" do
  180. @url "http://mastodon.example.org/@admin/99541947525187367"
  181. test "does not fetch unknown objects by default" do
  182. assert nil == Object.normalize(@url)
  183. end
  184. test "fetches unknown objects when fetch is explicitly true" do
  185. %Object{} = object = Object.normalize(@url, fetch: true)
  186. assert object.data["url"] == @url
  187. end
  188. test "does not fetch unknown objects when fetch is false" do
  189. assert is_nil(
  190. Object.normalize(@url,
  191. fetch: false
  192. )
  193. )
  194. end
  195. end
  196. describe "get_by_id_and_maybe_refetch" do
  197. setup do
  198. mock(fn
  199. %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
  200. %Tesla.Env{
  201. status: 200,
  202. body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
  203. headers: HttpRequestMock.activitypub_object_headers()
  204. }
  205. env ->
  206. apply(HttpRequestMock, :request, [env])
  207. end)
  208. mock_modified = fn resp ->
  209. mock(fn
  210. %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
  211. resp
  212. env ->
  213. apply(HttpRequestMock, :request, [env])
  214. end)
  215. end
  216. on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
  217. [mock_modified: mock_modified]
  218. end
  219. test "refetches if the time since the last refetch is greater than the interval", %{
  220. mock_modified: mock_modified
  221. } do
  222. %Object{} =
  223. object =
  224. Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
  225. fetch: true
  226. )
  227. Object.set_cache(object)
  228. assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
  229. assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  230. mock_modified.(%Tesla.Env{
  231. status: 200,
  232. body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
  233. headers: HttpRequestMock.activitypub_object_headers()
  234. })
  235. updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
  236. object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
  237. assert updated_object == object_in_cache
  238. assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
  239. assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
  240. end
  241. test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
  242. %Object{} =
  243. object =
  244. Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
  245. fetch: true
  246. )
  247. Object.set_cache(object)
  248. assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
  249. assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  250. assert capture_log(fn ->
  251. mock_modified.(%Tesla.Env{status: 404, body: ""})
  252. updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
  253. object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
  254. assert updated_object == object_in_cache
  255. assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
  256. assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  257. end) =~
  258. "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
  259. end
  260. test "does not refetch if the time since the last refetch is greater than the interval", %{
  261. mock_modified: mock_modified
  262. } do
  263. %Object{} =
  264. object =
  265. Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
  266. fetch: true
  267. )
  268. Object.set_cache(object)
  269. assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
  270. assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  271. mock_modified.(%Tesla.Env{
  272. status: 200,
  273. body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
  274. headers: HttpRequestMock.activitypub_object_headers()
  275. })
  276. updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
  277. object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
  278. assert updated_object == object_in_cache
  279. assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
  280. assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  281. end
  282. test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
  283. %Object{} =
  284. object =
  285. Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
  286. fetch: true
  287. )
  288. Object.set_cache(object)
  289. assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
  290. assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  291. user = insert(:user)
  292. activity = Activity.get_create_by_object_ap_id(object.data["id"])
  293. {:ok, activity} = CommonAPI.favorite(user, activity.id)
  294. object = Object.get_by_ap_id(activity.data["object"])
  295. assert object.data["like_count"] == 1
  296. mock_modified.(%Tesla.Env{
  297. status: 200,
  298. body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
  299. headers: HttpRequestMock.activitypub_object_headers()
  300. })
  301. updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
  302. object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
  303. assert updated_object == object_in_cache
  304. assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
  305. assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
  306. assert updated_object.data["like_count"] == 1
  307. end
  308. end
  309. describe ":hashtags association" do
  310. test "Hashtag records are created with Object record and updated on its change" do
  311. user = insert(:user)
  312. {:ok, %{object: object}} =
  313. CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
  314. assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
  315. Enum.sort_by(object.hashtags, & &1.name)
  316. {:ok, object} = Object.update_data(object, %{"tag" => []})
  317. assert [] = object.hashtags
  318. object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
  319. assert [] = object.hashtags
  320. {:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
  321. assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
  322. Enum.sort_by(object.hashtags, & &1.name)
  323. end
  324. end
  325. describe "get_emoji_reactions/1" do
  326. test "3-tuple current format" do
  327. object = %Object{
  328. data: %{
  329. "reactions" => [
  330. ["x", ["https://some/user"], "https://some/emoji"]
  331. ]
  332. }
  333. }
  334. assert Object.get_emoji_reactions(object) == object.data["reactions"]
  335. end
  336. test "2-tuple legacy format" do
  337. object = %Object{
  338. data: %{
  339. "reactions" => [
  340. ["x", ["https://some/user"]]
  341. ]
  342. }
  343. }
  344. assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]]
  345. end
  346. test "Map format" do
  347. object = %Object{
  348. data: %{
  349. "reactions" => %{
  350. "x" => ["https://some/user"]
  351. }
  352. }
  353. }
  354. assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]]
  355. end
  356. end
  357. end