Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

448 lines
15KB

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