Resolve follow activity from accept/reject without ID (#328)
[akkoma] / test / pleroma / object_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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