1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
85 Pleroma.Config.put([: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 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
119 Pleroma.Config.put([: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 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
153 Pleroma.Config.put([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe])
154 Pleroma.Config.put([: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 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
191 Pleroma.Config.put([: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 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
227 Pleroma.Config.put([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/")
228 Pleroma.Config.put([: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 test "fetches unknown objects by default" do
264 object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367")
266 assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367"
269 test "fetches unknown objects when fetch_remote is explicitly true" do
271 object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367", true)
273 assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367"
276 test "does not fetch unknown objects when fetch_remote is false" do
278 Object.normalize("http://mastodon.example.org/@admin/99541947525187367", false)
283 describe "get_by_id_and_maybe_refetch" do
286 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
289 body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
290 headers: HttpRequestMock.activitypub_object_headers()
294 apply(HttpRequestMock, :request, [env])
297 mock_modified = fn resp ->
299 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
303 apply(HttpRequestMock, :request, [env])
307 on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
309 [mock_modified: mock_modified]
312 test "refetches if the time since the last refetch is greater than the interval", %{
313 mock_modified: mock_modified
316 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
318 Object.set_cache(object)
320 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
321 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
323 mock_modified.(%Tesla.Env{
325 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
326 headers: HttpRequestMock.activitypub_object_headers()
329 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
330 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
331 assert updated_object == object_in_cache
332 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
333 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
336 test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
338 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
340 Object.set_cache(object)
342 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
343 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
345 assert capture_log(fn ->
346 mock_modified.(%Tesla.Env{status: 404, body: ""})
348 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
349 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
350 assert updated_object == object_in_cache
351 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
352 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
354 "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
357 test "does not refetch if the time since the last refetch is greater than the interval", %{
358 mock_modified: mock_modified
361 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
363 Object.set_cache(object)
365 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
366 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
368 mock_modified.(%Tesla.Env{
370 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
371 headers: HttpRequestMock.activitypub_object_headers()
374 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
375 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
376 assert updated_object == object_in_cache
377 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
378 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
381 test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
383 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
385 Object.set_cache(object)
387 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
388 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
391 activity = Activity.get_create_by_object_ap_id(object.data["id"])
392 {:ok, activity} = CommonAPI.favorite(user, activity.id)
393 object = Object.get_by_ap_id(activity.data["object"])
395 assert object.data["like_count"] == 1
397 mock_modified.(%Tesla.Env{
399 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
400 headers: HttpRequestMock.activitypub_object_headers()
403 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
404 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
405 assert updated_object == object_in_cache
406 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
407 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
409 assert updated_object.data["like_count"] == 1
413 describe ":hashtags association" do
414 test "Hashtag records are created with Object record and updated on its change" do
417 {:ok, %{object: object}} =
418 CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
420 assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
421 Enum.sort_by(object.hashtags, & &1.name)
423 {:ok, object} = Object.update_data(object, %{"tag" => []})
425 assert [] = object.hashtags
427 object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
428 assert [] = object.hashtags
430 {:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
432 assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
433 Enum.sort_by(object.hashtags, & &1.name)