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
10 import Pleroma.Factory
13 alias Pleroma.Activity
17 alias Pleroma.Tests.ObanHelpers
18 alias Pleroma.Web.CommonAPI
21 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
25 test "returns an object by it's AP id" do
26 object = insert(:note)
27 found_object = Object.get_by_ap_id(object.data["id"])
29 assert object == found_object
32 describe "generic changeset" do
33 test "it ensures uniqueness of the id" do
34 object = insert(:note)
35 cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}})
38 {:error, _result} = Repo.insert(cs)
42 describe "deletion function" do
43 test "deletes an object" do
44 object = insert(:note)
45 found_object = Object.get_by_ap_id(object.data["id"])
47 assert object == found_object
49 Object.delete(found_object)
51 found_object = Object.get_by_ap_id(object.data["id"])
53 refute object == found_object
55 assert found_object.data["type"] == "Tombstone"
58 test "ensures cache is cleared for the object" do
59 object = insert(:note)
60 cached_object = Object.get_cached_by_ap_id(object.data["id"])
62 assert object == cached_object
64 Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe")
66 Object.delete(cached_object)
68 {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}")
69 {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path)
71 cached_object = Object.get_cached_by_ap_id(object.data["id"])
73 refute object == cached_object
75 assert cached_object.data["type"] == "Tombstone"
79 describe "delete attachments" do
80 setup do: clear_config([Pleroma.Upload])
81 setup do: clear_config([:instance, :cleanup_attachments])
83 test "Disabled via config" do
84 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
85 clear_config([:instance, :cleanup_attachments], false)
88 content_type: "image/jpeg",
89 path: Path.absname("test/fixtures/image.jpg"),
90 filename: "an_image.jpg"
95 {:ok, %Object{} = attachment} =
96 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
98 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
99 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
101 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
103 path = href |> Path.dirname() |> Path.basename()
105 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
109 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
111 assert Object.get_by_id(note.id).data["deleted"]
112 refute Object.get_by_id(attachment.id) == nil
114 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
117 test "in subdirectories" do
118 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
119 clear_config([:instance, :cleanup_attachments], true)
122 content_type: "image/jpeg",
123 path: Path.absname("test/fixtures/image.jpg"),
124 filename: "an_image.jpg"
129 {:ok, %Object{} = attachment} =
130 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
132 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
133 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
135 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
137 path = href |> Path.dirname() |> Path.basename()
139 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
143 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
145 assert Object.get_by_id(note.id).data["deleted"]
146 assert Object.get_by_id(attachment.id) == nil
148 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
151 test "with dedupe enabled" do
152 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
153 clear_config([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe])
154 clear_config([:instance, :cleanup_attachments], true)
156 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
158 File.mkdir_p!(uploads_dir)
161 content_type: "image/jpeg",
162 path: Path.absname("test/fixtures/image.jpg"),
163 filename: "an_image.jpg"
168 {:ok, %Object{} = attachment} =
169 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
171 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
172 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
174 filename = Path.basename(href)
176 assert {:ok, files} = File.ls(uploads_dir)
177 assert filename in files
181 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
183 assert Object.get_by_id(note.id).data["deleted"]
184 assert Object.get_by_id(attachment.id) == nil
185 assert {:ok, files} = File.ls(uploads_dir)
186 refute filename in files
189 test "with objects that have legacy data.url attribute" do
190 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
191 clear_config([:instance, :cleanup_attachments], true)
194 content_type: "image/jpeg",
195 path: Path.absname("test/fixtures/image.jpg"),
196 filename: "an_image.jpg"
201 {:ok, %Object{} = attachment} =
202 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
204 {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id})
206 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
207 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
209 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
211 path = href |> Path.dirname() |> Path.basename()
213 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
217 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
219 assert Object.get_by_id(note.id).data["deleted"]
220 assert Object.get_by_id(attachment.id) == nil
222 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
225 test "With custom base_url" do
226 clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
227 clear_config([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/")
228 clear_config([:instance, :cleanup_attachments], true)
231 content_type: "image/jpeg",
232 path: Path.absname("test/fixtures/image.jpg"),
233 filename: "an_image.jpg"
238 {:ok, %Object{} = attachment} =
239 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
241 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
242 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
244 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
246 path = href |> Path.dirname() |> Path.basename()
248 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
252 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
254 assert Object.get_by_id(note.id).data["deleted"]
255 assert Object.get_by_id(attachment.id) == nil
257 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
261 describe "normalizer" do
262 @url "http://mastodon.example.org/@admin/99541947525187367"
263 test "does not fetch unknown objects by default" do
264 assert nil == Object.normalize(@url)
267 test "fetches unknown objects when fetch is explicitly true" do
268 %Object{} = object = Object.normalize(@url, fetch: true)
270 assert object.data["url"] == @url
273 test "does not fetch unknown objects when fetch is false" do
275 Object.normalize(@url,
282 describe "get_by_id_and_maybe_refetch" do
285 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
288 body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
289 headers: HttpRequestMock.activitypub_object_headers()
293 apply(HttpRequestMock, :request, [env])
296 mock_modified = fn resp ->
298 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
302 apply(HttpRequestMock, :request, [env])
306 on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
308 [mock_modified: mock_modified]
311 test "refetches if the time since the last refetch is greater than the interval", %{
312 mock_modified: mock_modified
316 Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
320 Object.set_cache(object)
322 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
323 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
325 mock_modified.(%Tesla.Env{
327 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
328 headers: HttpRequestMock.activitypub_object_headers()
331 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
332 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
333 assert updated_object == object_in_cache
334 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
335 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
338 test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
341 Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
345 Object.set_cache(object)
347 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
348 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
350 assert capture_log(fn ->
351 mock_modified.(%Tesla.Env{status: 404, body: ""})
353 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
354 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
355 assert updated_object == object_in_cache
356 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
357 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
359 "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
362 test "does not refetch if the time since the last refetch is greater than the interval", %{
363 mock_modified: mock_modified
367 Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
371 Object.set_cache(object)
373 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
374 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
376 mock_modified.(%Tesla.Env{
378 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
379 headers: HttpRequestMock.activitypub_object_headers()
382 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
383 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
384 assert updated_object == object_in_cache
385 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
386 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
389 test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
392 Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
396 Object.set_cache(object)
398 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
399 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
402 activity = Activity.get_create_by_object_ap_id(object.data["id"])
403 {:ok, activity} = CommonAPI.favorite(user, activity.id)
404 object = Object.get_by_ap_id(activity.data["object"])
406 assert object.data["like_count"] == 1
408 mock_modified.(%Tesla.Env{
410 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
411 headers: HttpRequestMock.activitypub_object_headers()
414 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
415 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
416 assert updated_object == object_in_cache
417 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
418 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
420 assert updated_object.data["like_count"] == 1
424 describe ":hashtags association" do
425 test "Hashtag records are created with Object record and updated on its change" do
428 {:ok, %{object: object}} =
429 CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
431 assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
432 Enum.sort_by(object.hashtags, & &1.name)
434 {:ok, object} = Object.update_data(object, %{"tag" => []})
436 assert [] = object.hashtags
438 object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
439 assert [] = object.hashtags
441 {:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
443 assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
444 Enum.sort_by(object.hashtags, & &1.name)