1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.ObjectTest do
7 use Oban.Testing, repo: Pleroma.Repo
9 import ExUnit.CaptureLog
11 import Pleroma.Factory
14 alias Pleroma.Activity
18 alias Pleroma.Tests.ObanHelpers
19 alias Pleroma.UnstubbedConfigMock, as: ConfigMock
20 alias Pleroma.Web.CommonAPI
23 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
24 ConfigMock |> stub_with(Pleroma.Test.StaticConfig)
28 test "returns an object by it's AP id" do
29 object = insert(:note)
30 found_object = Object.get_by_ap_id(object.data["id"])
32 assert object == found_object
35 describe "generic changeset" do
36 test "it ensures uniqueness of the id" do
37 object = insert(:note)
38 cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}})
41 {:error, _result} = Repo.insert(cs)
45 describe "deletion function" do
46 test "deletes an object" do
47 object = insert(:note)
48 found_object = Object.get_by_ap_id(object.data["id"])
50 assert object == found_object
52 Object.delete(found_object)
54 found_object = Object.get_by_ap_id(object.data["id"])
56 refute object == found_object
58 assert found_object.data["type"] == "Tombstone"
61 test "ensures cache is cleared for the object" do
62 object = insert(:note)
63 cached_object = Object.get_cached_by_ap_id(object.data["id"])
65 assert object == cached_object
67 Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe")
69 Object.delete(cached_object)
71 {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}")
72 {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path)
74 cached_object = Object.get_cached_by_ap_id(object.data["id"])
76 refute object == cached_object
78 assert cached_object.data["type"] == "Tombstone"
82 describe "delete attachments" do
83 setup do: clear_config([Pleroma.Upload])
84 setup do: clear_config([:instance, :cleanup_attachments])
86 test "Disabled via config" do
87 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
88 clear_config([:instance, :cleanup_attachments], false)
91 content_type: "image/jpeg",
92 path: Path.absname("test/fixtures/image.jpg"),
93 filename: "an_image.jpg"
98 {:ok, %Object{} = attachment} =
99 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
101 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
102 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
104 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
106 path = href |> Path.dirname() |> Path.basename()
108 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
112 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
114 assert Object.get_by_id(note.id).data["deleted"]
115 refute Object.get_by_id(attachment.id) == nil
117 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
120 test "in subdirectories" do
121 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
122 clear_config([:instance, :cleanup_attachments], true)
125 content_type: "image/jpeg",
126 path: Path.absname("test/fixtures/image.jpg"),
127 filename: "an_image.jpg"
132 {:ok, %Object{} = attachment} =
133 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
135 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
136 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
138 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
140 path = href |> Path.dirname() |> Path.basename()
142 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
146 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
151 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
154 test "with dedupe enabled" do
155 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
156 clear_config([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe])
157 clear_config([:instance, :cleanup_attachments], true)
159 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
161 File.mkdir_p!(uploads_dir)
164 content_type: "image/jpeg",
165 path: Path.absname("test/fixtures/image.jpg"),
166 filename: "an_image.jpg"
171 {:ok, %Object{} = attachment} =
172 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
174 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
175 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
177 filename = Path.basename(href)
179 assert {:ok, files} = File.ls(uploads_dir)
180 assert filename in files
184 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
186 assert Object.get_by_id(note.id).data["deleted"]
187 assert Object.get_by_id(attachment.id) == nil
188 assert {:ok, files} = File.ls(uploads_dir)
189 refute filename in files
192 test "with objects that have legacy data.url attribute" do
193 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
194 clear_config([:instance, :cleanup_attachments], true)
197 content_type: "image/jpeg",
198 path: Path.absname("test/fixtures/image.jpg"),
199 filename: "an_image.jpg"
204 {:ok, %Object{} = attachment} =
205 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
207 {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id})
209 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
210 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
212 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
214 path = href |> Path.dirname() |> Path.basename()
216 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
220 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
222 assert Object.get_by_id(note.id).data["deleted"]
223 assert Object.get_by_id(attachment.id) == nil
225 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
228 test "With custom base_url" do
229 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
230 clear_config([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/")
231 clear_config([:instance, :cleanup_attachments], true)
234 content_type: "image/jpeg",
235 path: Path.absname("test/fixtures/image.jpg"),
236 filename: "an_image.jpg"
241 {:ok, %Object{} = attachment} =
242 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
244 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
245 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
247 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
249 path = href |> Path.dirname() |> Path.basename()
251 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
255 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
257 assert Object.get_by_id(note.id).data["deleted"]
258 assert Object.get_by_id(attachment.id) == nil
260 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
264 describe "normalizer" do
265 @url "http://mastodon.example.org/@admin/99541947525187367"
266 test "does not fetch unknown objects by default" do
267 assert nil == Object.normalize(@url)
270 test "fetches unknown objects when fetch is explicitly true" do
271 %Object{} = object = Object.normalize(@url, fetch: true)
273 assert object.data["url"] == @url
276 test "does not fetch unknown objects when fetch is false" do
278 Object.normalize(@url,
285 describe "get_by_id_and_maybe_refetch" do
288 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
291 body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
292 headers: HttpRequestMock.activitypub_object_headers()
296 apply(HttpRequestMock, :request, [env])
299 mock_modified = fn resp ->
301 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
305 apply(HttpRequestMock, :request, [env])
309 on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
311 [mock_modified: mock_modified]
314 test "refetches if the time since the last refetch is greater than the interval", %{
315 mock_modified: mock_modified
319 Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
323 Object.set_cache(object)
325 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
326 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
328 mock_modified.(%Tesla.Env{
330 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
331 headers: HttpRequestMock.activitypub_object_headers()
334 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
335 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
336 assert updated_object == object_in_cache
337 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
338 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
341 test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
344 Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
348 Object.set_cache(object)
350 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
351 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
353 assert capture_log(fn ->
354 mock_modified.(%Tesla.Env{status: 404, body: ""})
356 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
357 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
358 assert updated_object == object_in_cache
359 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
360 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
362 "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
365 test "does not refetch if the time since the last refetch is greater than the interval", %{
366 mock_modified: mock_modified
370 Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
374 Object.set_cache(object)
376 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
377 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
379 mock_modified.(%Tesla.Env{
381 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
382 headers: HttpRequestMock.activitypub_object_headers()
385 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
386 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
387 assert updated_object == object_in_cache
388 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
389 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
392 test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
395 Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
399 Object.set_cache(object)
401 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
402 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
405 activity = Activity.get_create_by_object_ap_id(object.data["id"])
406 {:ok, activity} = CommonAPI.favorite(user, activity.id)
407 object = Object.get_by_ap_id(activity.data["object"])
409 assert object.data["like_count"] == 1
411 mock_modified.(%Tesla.Env{
413 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
414 headers: HttpRequestMock.activitypub_object_headers()
417 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
418 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
419 assert updated_object == object_in_cache
420 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
421 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
423 assert updated_object.data["like_count"] == 1
427 describe ":hashtags association" do
428 test "Hashtag records are created with Object record and updated on its change" do
431 {:ok, %{object: object}} =
432 CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
434 assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
435 Enum.sort_by(object.hashtags, & &1.name)
437 {:ok, object} = Object.update_data(object, %{"tag" => []})
439 assert [] = object.hashtags
441 object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
442 assert [] = object.hashtags
444 {:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
446 assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
447 Enum.sort_by(object.hashtags, & &1.name)
451 describe "get_emoji_reactions/1" do
452 test "3-tuple current format" do
456 ["x", ["https://some/user"], "https://some/emoji"]
461 assert Object.get_emoji_reactions(object) == object.data["reactions"]
464 test "2-tuple legacy format" do
468 ["x", ["https://some/user"]]
473 assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]]
480 "x" => ["https://some/user"]
485 assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]]