First
[anni] / test / pleroma / object_test.exs
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
5 defmodule Pleroma.ObjectTest do
6   use Pleroma.DataCase
7   use Oban.Testing, repo: Pleroma.Repo
8
9   import ExUnit.CaptureLog
10   import Pleroma.Factory
11   import Tesla.Mock
12
13   alias Pleroma.Activity
14   alias Pleroma.Hashtag
15   alias Pleroma.Object
16   alias Pleroma.Repo
17   alias Pleroma.Tests.ObanHelpers
18   alias Pleroma.Web.CommonAPI
19
20   setup do
21     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
22     :ok
23   end
24
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"])
28
29     assert object == found_object
30   end
31
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"]}})
36       assert cs.valid?
37
38       {:error, _result} = Repo.insert(cs)
39     end
40   end
41
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"])
46
47       assert object == found_object
48
49       Object.delete(found_object)
50
51       found_object = Object.get_by_ap_id(object.data["id"])
52
53       refute object == found_object
54
55       assert found_object.data["type"] == "Tombstone"
56     end
57
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"])
61
62       assert object == cached_object
63
64       Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe")
65
66       Object.delete(cached_object)
67
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)
70
71       cached_object = Object.get_cached_by_ap_id(object.data["id"])
72
73       refute object == cached_object
74
75       assert cached_object.data["type"] == "Tombstone"
76     end
77   end
78
79   describe "delete attachments" do
80     setup do: clear_config([Pleroma.Upload])
81     setup do: clear_config([:instance, :cleanup_attachments])
82
83     test "Disabled via config" do
84       clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
85       clear_config([:instance, :cleanup_attachments], false)
86
87       file = %Plug.Upload{
88         content_type: "image/jpeg",
89         path: Path.absname("test/fixtures/image.jpg"),
90         filename: "an_image.jpg"
91       }
92
93       user = insert(:user)
94
95       {:ok, %Object{} = attachment} =
96         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
97
98       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
99         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
100
101       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
102
103       path = href |> Path.dirname() |> Path.basename()
104
105       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
106
107       Object.delete(note)
108
109       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
110
111       assert Object.get_by_id(note.id).data["deleted"]
112       refute Object.get_by_id(attachment.id) == nil
113
114       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
115     end
116
117     test "in subdirectories" do
118       clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
119       clear_config([:instance, :cleanup_attachments], true)
120
121       file = %Plug.Upload{
122         content_type: "image/jpeg",
123         path: Path.absname("test/fixtures/image.jpg"),
124         filename: "an_image.jpg"
125       }
126
127       user = insert(:user)
128
129       {:ok, %Object{} = attachment} =
130         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
131
132       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
133         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
134
135       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
136
137       path = href |> Path.dirname() |> Path.basename()
138
139       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
140
141       Object.delete(note)
142
143       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
144
145       assert Object.get_by_id(note.id).data["deleted"]
146       assert Object.get_by_id(attachment.id) == nil
147
148       assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
149     end
150
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)
155
156       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
157
158       File.mkdir_p!(uploads_dir)
159
160       file = %Plug.Upload{
161         content_type: "image/jpeg",
162         path: Path.absname("test/fixtures/image.jpg"),
163         filename: "an_image.jpg"
164       }
165
166       user = insert(:user)
167
168       {:ok, %Object{} = attachment} =
169         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
170
171       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
172         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
173
174       filename = Path.basename(href)
175
176       assert {:ok, files} = File.ls(uploads_dir)
177       assert filename in files
178
179       Object.delete(note)
180
181       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
182
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
187     end
188
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)
192
193       file = %Plug.Upload{
194         content_type: "image/jpeg",
195         path: Path.absname("test/fixtures/image.jpg"),
196         filename: "an_image.jpg"
197       }
198
199       user = insert(:user)
200
201       {:ok, %Object{} = attachment} =
202         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
203
204       {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id})
205
206       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
207         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
208
209       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
210
211       path = href |> Path.dirname() |> Path.basename()
212
213       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
214
215       Object.delete(note)
216
217       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
218
219       assert Object.get_by_id(note.id).data["deleted"]
220       assert Object.get_by_id(attachment.id) == nil
221
222       assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
223     end
224
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)
229
230       file = %Plug.Upload{
231         content_type: "image/jpeg",
232         path: Path.absname("test/fixtures/image.jpg"),
233         filename: "an_image.jpg"
234       }
235
236       user = insert(:user)
237
238       {:ok, %Object{} = attachment} =
239         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
240
241       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
242         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
243
244       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
245
246       path = href |> Path.dirname() |> Path.basename()
247
248       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
249
250       Object.delete(note)
251
252       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
253
254       assert Object.get_by_id(note.id).data["deleted"]
255       assert Object.get_by_id(attachment.id) == nil
256
257       assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
258     end
259   end
260
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)
265     end
266
267     test "fetches unknown objects when fetch is explicitly true" do
268       %Object{} = object = Object.normalize(@url, fetch: true)
269
270       assert object.data["url"] == @url
271     end
272
273     test "does not fetch unknown objects when fetch is false" do
274       assert is_nil(
275                Object.normalize(@url,
276                  fetch: false
277                )
278              )
279     end
280   end
281
282   describe "get_by_id_and_maybe_refetch" do
283     setup do
284       mock(fn
285         %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
286           %Tesla.Env{
287             status: 200,
288             body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
289             headers: HttpRequestMock.activitypub_object_headers()
290           }
291
292         env ->
293           apply(HttpRequestMock, :request, [env])
294       end)
295
296       mock_modified = fn resp ->
297         mock(fn
298           %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
299             resp
300
301           env ->
302             apply(HttpRequestMock, :request, [env])
303         end)
304       end
305
306       on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
307
308       [mock_modified: mock_modified]
309     end
310
311     test "refetches if the time since the last refetch is greater than the interval", %{
312       mock_modified: mock_modified
313     } do
314       %Object{} =
315         object =
316         Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
317           fetch: true
318         )
319
320       Object.set_cache(object)
321
322       assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
323       assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
324
325       mock_modified.(%Tesla.Env{
326         status: 200,
327         body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
328         headers: HttpRequestMock.activitypub_object_headers()
329       })
330
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
336     end
337
338     test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
339       %Object{} =
340         object =
341         Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
342           fetch: true
343         )
344
345       Object.set_cache(object)
346
347       assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
348       assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
349
350       assert capture_log(fn ->
351                mock_modified.(%Tesla.Env{status: 404, body: ""})
352
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
358              end) =~
359                "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
360     end
361
362     test "does not refetch if the time since the last refetch is greater than the interval", %{
363       mock_modified: mock_modified
364     } do
365       %Object{} =
366         object =
367         Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
368           fetch: true
369         )
370
371       Object.set_cache(object)
372
373       assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
374       assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
375
376       mock_modified.(%Tesla.Env{
377         status: 200,
378         body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
379         headers: HttpRequestMock.activitypub_object_headers()
380       })
381
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
387     end
388
389     test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
390       %Object{} =
391         object =
392         Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
393           fetch: true
394         )
395
396       Object.set_cache(object)
397
398       assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
399       assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
400
401       user = insert(:user)
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"])
405
406       assert object.data["like_count"] == 1
407
408       mock_modified.(%Tesla.Env{
409         status: 200,
410         body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
411         headers: HttpRequestMock.activitypub_object_headers()
412       })
413
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
419
420       assert updated_object.data["like_count"] == 1
421     end
422   end
423
424   describe ":hashtags association" do
425     test "Hashtag records are created with Object record and updated on its change" do
426       user = insert(:user)
427
428       {:ok, %{object: object}} =
429         CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
430
431       assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
432                Enum.sort_by(object.hashtags, & &1.name)
433
434       {:ok, object} = Object.update_data(object, %{"tag" => []})
435
436       assert [] = object.hashtags
437
438       object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
439       assert [] = object.hashtags
440
441       {:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
442
443       assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
444                Enum.sort_by(object.hashtags, & &1.name)
445     end
446   end
447 end