Merge remote-tracking branch 'remotes/origin/develop' into feature/object-hashtags...
[akkoma] / test / pleroma / object_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
85 Pleroma.Config.put([: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 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
119 Pleroma.Config.put([: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 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)
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 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
191 Pleroma.Config.put([: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 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)
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 test "fetches unknown objects by default" do
263 %Object{} =
264 object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367")
265
266 assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367"
267 end
268
269 test "fetches unknown objects when fetch_remote is explicitly true" do
270 %Object{} =
271 object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367", true)
272
273 assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367"
274 end
275
276 test "does not fetch unknown objects when fetch_remote is false" do
277 assert is_nil(
278 Object.normalize("http://mastodon.example.org/@admin/99541947525187367", false)
279 )
280 end
281 end
282
283 describe "get_by_id_and_maybe_refetch" do
284 setup do
285 mock(fn
286 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
287 %Tesla.Env{
288 status: 200,
289 body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
290 headers: HttpRequestMock.activitypub_object_headers()
291 }
292
293 env ->
294 apply(HttpRequestMock, :request, [env])
295 end)
296
297 mock_modified = fn resp ->
298 mock(fn
299 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
300 resp
301
302 env ->
303 apply(HttpRequestMock, :request, [env])
304 end)
305 end
306
307 on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
308
309 [mock_modified: mock_modified]
310 end
311
312 test "refetches if the time since the last refetch is greater than the interval", %{
313 mock_modified: mock_modified
314 } do
315 %Object{} =
316 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
317
318 Object.set_cache(object)
319
320 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
321 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
322
323 mock_modified.(%Tesla.Env{
324 status: 200,
325 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
326 headers: HttpRequestMock.activitypub_object_headers()
327 })
328
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
334 end
335
336 test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
337 %Object{} =
338 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
339
340 Object.set_cache(object)
341
342 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
343 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
344
345 assert capture_log(fn ->
346 mock_modified.(%Tesla.Env{status: 404, body: ""})
347
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
353 end) =~
354 "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
355 end
356
357 test "does not refetch if the time since the last refetch is greater than the interval", %{
358 mock_modified: mock_modified
359 } do
360 %Object{} =
361 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
362
363 Object.set_cache(object)
364
365 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
366 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
367
368 mock_modified.(%Tesla.Env{
369 status: 200,
370 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
371 headers: HttpRequestMock.activitypub_object_headers()
372 })
373
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
379 end
380
381 test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
382 %Object{} =
383 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
384
385 Object.set_cache(object)
386
387 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
388 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
389
390 user = insert(:user)
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"])
394
395 assert object.data["like_count"] == 1
396
397 mock_modified.(%Tesla.Env{
398 status: 200,
399 body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
400 headers: HttpRequestMock.activitypub_object_headers()
401 })
402
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
408
409 assert updated_object.data["like_count"] == 1
410 end
411 end
412
413 describe ":hashtags association" do
414 test "Hashtag records are created with Object record and updated on its change" do
415 user = insert(:user)
416
417 {:ok, %{object: object}} =
418 CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
419
420 assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
421 Enum.sort_by(object.hashtags, & &1.name)
422
423 {:ok, object} = Object.update_data(object, %{"tag" => []})
424
425 assert [] = object.hashtags
426
427 object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
428 assert [] = object.hashtags
429
430 {:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
431
432 assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
433 Enum.sort_by(object.hashtags, & &1.name)
434 end
435 end
436 end