total rebase
[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 Mox
11   import Pleroma.Factory
12   import Tesla.Mock
13
14   alias Pleroma.Activity
15   alias Pleroma.Hashtag
16   alias Pleroma.Object
17   alias Pleroma.Repo
18   alias Pleroma.Tests.ObanHelpers
19   alias Pleroma.UnstubbedConfigMock, as: ConfigMock
20   alias Pleroma.Web.CommonAPI
21
22   setup do
23     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
24     ConfigMock |> stub_with(Pleroma.Test.StaticConfig)
25     :ok
26   end
27
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"])
31
32     assert object == found_object
33   end
34
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"]}})
39       assert cs.valid?
40
41       {:error, _result} = Repo.insert(cs)
42     end
43   end
44
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"])
49
50       assert object == found_object
51
52       Object.delete(found_object)
53
54       found_object = Object.get_by_ap_id(object.data["id"])
55
56       refute object == found_object
57
58       assert found_object.data["type"] == "Tombstone"
59     end
60
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"])
64
65       assert object == cached_object
66
67       Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe")
68
69       Object.delete(cached_object)
70
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)
73
74       cached_object = Object.get_cached_by_ap_id(object.data["id"])
75
76       refute object == cached_object
77
78       assert cached_object.data["type"] == "Tombstone"
79     end
80   end
81
82   describe "delete attachments" do
83     setup do: clear_config([Pleroma.Upload])
84     setup do: clear_config([:instance, :cleanup_attachments])
85
86     test "Disabled via config" do
87       clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
88       clear_config([:instance, :cleanup_attachments], false)
89
90       file = %Plug.Upload{
91         content_type: "image/jpeg",
92         path: Path.absname("test/fixtures/image.jpg"),
93         filename: "an_image.jpg"
94       }
95
96       user = insert(:user)
97
98       {:ok, %Object{} = attachment} =
99         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
100
101       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
102         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
103
104       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
105
106       path = href |> Path.dirname() |> Path.basename()
107
108       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
109
110       Object.delete(note)
111
112       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
113
114       assert Object.get_by_id(note.id).data["deleted"]
115       refute Object.get_by_id(attachment.id) == nil
116
117       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
118     end
119
120     test "in subdirectories" do
121       clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
122       clear_config([:instance, :cleanup_attachments], true)
123
124       file = %Plug.Upload{
125         content_type: "image/jpeg",
126         path: Path.absname("test/fixtures/image.jpg"),
127         filename: "an_image.jpg"
128       }
129
130       user = insert(:user)
131
132       {:ok, %Object{} = attachment} =
133         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
134
135       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
136         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
137
138       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
139
140       path = href |> Path.dirname() |> Path.basename()
141
142       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
143
144       Object.delete(note)
145
146       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
147
148       assert Object.get_by_id(note.id).data["deleted"]
149       assert Object.get_by_id(attachment.id) == nil
150
151       assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
152     end
153
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)
158
159       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
160
161       File.mkdir_p!(uploads_dir)
162
163       file = %Plug.Upload{
164         content_type: "image/jpeg",
165         path: Path.absname("test/fixtures/image.jpg"),
166         filename: "an_image.jpg"
167       }
168
169       user = insert(:user)
170
171       {:ok, %Object{} = attachment} =
172         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
173
174       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
175         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
176
177       filename = Path.basename(href)
178
179       assert {:ok, files} = File.ls(uploads_dir)
180       assert filename in files
181
182       Object.delete(note)
183
184       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
185
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
190     end
191
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)
195
196       file = %Plug.Upload{
197         content_type: "image/jpeg",
198         path: Path.absname("test/fixtures/image.jpg"),
199         filename: "an_image.jpg"
200       }
201
202       user = insert(:user)
203
204       {:ok, %Object{} = attachment} =
205         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
206
207       {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id})
208
209       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
210         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
211
212       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
213
214       path = href |> Path.dirname() |> Path.basename()
215
216       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
217
218       Object.delete(note)
219
220       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
221
222       assert Object.get_by_id(note.id).data["deleted"]
223       assert Object.get_by_id(attachment.id) == nil
224
225       assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
226     end
227
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)
232
233       file = %Plug.Upload{
234         content_type: "image/jpeg",
235         path: Path.absname("test/fixtures/image.jpg"),
236         filename: "an_image.jpg"
237       }
238
239       user = insert(:user)
240
241       {:ok, %Object{} = attachment} =
242         Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
243
244       %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
245         note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
246
247       uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
248
249       path = href |> Path.dirname() |> Path.basename()
250
251       assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
252
253       Object.delete(note)
254
255       ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
256
257       assert Object.get_by_id(note.id).data["deleted"]
258       assert Object.get_by_id(attachment.id) == nil
259
260       assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
261     end
262   end
263
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)
268     end
269
270     test "fetches unknown objects when fetch is explicitly true" do
271       %Object{} = object = Object.normalize(@url, fetch: true)
272
273       assert object.data["url"] == @url
274     end
275
276     test "does not fetch unknown objects when fetch is false" do
277       assert is_nil(
278                Object.normalize(@url,
279                  fetch: false
280                )
281              )
282     end
283   end
284
285   describe "get_by_id_and_maybe_refetch" do
286     setup do
287       mock(fn
288         %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
289           %Tesla.Env{
290             status: 200,
291             body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
292             headers: HttpRequestMock.activitypub_object_headers()
293           }
294
295         env ->
296           apply(HttpRequestMock, :request, [env])
297       end)
298
299       mock_modified = fn resp ->
300         mock(fn
301           %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
302             resp
303
304           env ->
305             apply(HttpRequestMock, :request, [env])
306         end)
307       end
308
309       on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
310
311       [mock_modified: mock_modified]
312     end
313
314     test "refetches if the time since the last refetch is greater than the interval", %{
315       mock_modified: mock_modified
316     } do
317       %Object{} =
318         object =
319         Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
320           fetch: true
321         )
322
323       Object.set_cache(object)
324
325       assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
326       assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
327
328       mock_modified.(%Tesla.Env{
329         status: 200,
330         body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
331         headers: HttpRequestMock.activitypub_object_headers()
332       })
333
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
339     end
340
341     test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
342       %Object{} =
343         object =
344         Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
345           fetch: true
346         )
347
348       Object.set_cache(object)
349
350       assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
351       assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
352
353       assert capture_log(fn ->
354                mock_modified.(%Tesla.Env{status: 404, body: ""})
355
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
361              end) =~
362                "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
363     end
364
365     test "does not refetch if the time since the last refetch is greater than the interval", %{
366       mock_modified: mock_modified
367     } do
368       %Object{} =
369         object =
370         Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
371           fetch: true
372         )
373
374       Object.set_cache(object)
375
376       assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
377       assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
378
379       mock_modified.(%Tesla.Env{
380         status: 200,
381         body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
382         headers: HttpRequestMock.activitypub_object_headers()
383       })
384
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
390     end
391
392     test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
393       %Object{} =
394         object =
395         Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
396           fetch: true
397         )
398
399       Object.set_cache(object)
400
401       assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
402       assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
403
404       user = insert(:user)
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"])
408
409       assert object.data["like_count"] == 1
410
411       mock_modified.(%Tesla.Env{
412         status: 200,
413         body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
414         headers: HttpRequestMock.activitypub_object_headers()
415       })
416
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
422
423       assert updated_object.data["like_count"] == 1
424     end
425   end
426
427   describe ":hashtags association" do
428     test "Hashtag records are created with Object record and updated on its change" do
429       user = insert(:user)
430
431       {:ok, %{object: object}} =
432         CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
433
434       assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
435                Enum.sort_by(object.hashtags, & &1.name)
436
437       {:ok, object} = Object.update_data(object, %{"tag" => []})
438
439       assert [] = object.hashtags
440
441       object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
442       assert [] = object.hashtags
443
444       {:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
445
446       assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
447                Enum.sort_by(object.hashtags, & &1.name)
448     end
449   end
450
451   describe "get_emoji_reactions/1" do
452     test "3-tuple current format" do
453       object = %Object{
454         data: %{
455           "reactions" => [
456             ["x", ["https://some/user"], "https://some/emoji"]
457           ]
458         }
459       }
460
461       assert Object.get_emoji_reactions(object) == object.data["reactions"]
462     end
463
464     test "2-tuple legacy format" do
465       object = %Object{
466         data: %{
467           "reactions" => [
468             ["x", ["https://some/user"]]
469           ]
470         }
471       }
472
473       assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]]
474     end
475
476     test "Map format" do
477       object = %Object{
478         data: %{
479           "reactions" => %{
480             "x" => ["https://some/user"]
481           }
482         }
483       }
484
485       assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]]
486     end
487   end
488 end