Merge branch 'feature/make_attachments_cleanup_optional' into 'develop'
[akkoma] / test / object_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 import ExUnit.CaptureLog
9 import Pleroma.Factory
10 import Tesla.Mock
11 alias Pleroma.Activity
12 alias Pleroma.Object
13 alias Pleroma.Repo
14 alias Pleroma.Tests.ObanHelpers
15 alias Pleroma.Web.CommonAPI
16
17 setup do
18 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
19 :ok
20 end
21
22 test "returns an object by it's AP id" do
23 object = insert(:note)
24 found_object = Object.get_by_ap_id(object.data["id"])
25
26 assert object == found_object
27 end
28
29 describe "generic changeset" do
30 test "it ensures uniqueness of the id" do
31 object = insert(:note)
32 cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}})
33 assert cs.valid?
34
35 {:error, _result} = Repo.insert(cs)
36 end
37 end
38
39 describe "deletion function" do
40 test "deletes an object" do
41 object = insert(:note)
42 found_object = Object.get_by_ap_id(object.data["id"])
43
44 assert object == found_object
45
46 Object.delete(found_object)
47
48 found_object = Object.get_by_ap_id(object.data["id"])
49
50 refute object == found_object
51
52 assert found_object.data["type"] == "Tombstone"
53 end
54
55 test "ensures cache is cleared for the object" do
56 object = insert(:note)
57 cached_object = Object.get_cached_by_ap_id(object.data["id"])
58
59 assert object == cached_object
60
61 Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe")
62
63 Object.delete(cached_object)
64
65 {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}")
66 {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path)
67
68 cached_object = Object.get_cached_by_ap_id(object.data["id"])
69
70 refute object == cached_object
71
72 assert cached_object.data["type"] == "Tombstone"
73 end
74 end
75
76 describe "delete attachments" do
77 clear_config([Pleroma.Upload])
78
79 test "Disabled via config" do
80 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
81 Pleroma.Config.put([:instance, :cleanup_attachments], false)
82
83 file = %Plug.Upload{
84 content_type: "image/jpg",
85 path: Path.absname("test/fixtures/image.jpg"),
86 filename: "an_image.jpg"
87 }
88
89 user = insert(:user)
90
91 {:ok, %Object{} = attachment} =
92 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
93
94 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
95 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
96
97 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
98
99 path = href |> Path.dirname() |> Path.basename()
100
101 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
102
103 Object.delete(note)
104
105 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
106
107 assert Object.get_by_id(note.id).data["deleted"]
108 refute Object.get_by_id(attachment.id) == nil
109
110 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
111 end
112
113 test "in subdirectories" do
114 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
115 Pleroma.Config.put([:instance, :cleanup_attachments], true)
116
117 file = %Plug.Upload{
118 content_type: "image/jpg",
119 path: Path.absname("test/fixtures/image.jpg"),
120 filename: "an_image.jpg"
121 }
122
123 user = insert(:user)
124
125 {:ok, %Object{} = attachment} =
126 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
127
128 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
129 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
130
131 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
132
133 path = href |> Path.dirname() |> Path.basename()
134
135 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
136
137 Object.delete(note)
138
139 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
140
141 assert Object.get_by_id(note.id).data["deleted"]
142 assert Object.get_by_id(attachment.id) == nil
143
144 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
145 end
146
147 test "with dedupe enabled" do
148 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
149 Pleroma.Config.put([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe])
150 Pleroma.Config.put([:instance, :cleanup_attachments], true)
151
152 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
153
154 File.mkdir_p!(uploads_dir)
155
156 file = %Plug.Upload{
157 content_type: "image/jpg",
158 path: Path.absname("test/fixtures/image.jpg"),
159 filename: "an_image.jpg"
160 }
161
162 user = insert(:user)
163
164 {:ok, %Object{} = attachment} =
165 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
166
167 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
168 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
169
170 filename = Path.basename(href)
171
172 assert {:ok, files} = File.ls(uploads_dir)
173 assert filename in files
174
175 Object.delete(note)
176
177 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
178
179 assert Object.get_by_id(note.id).data["deleted"]
180 assert Object.get_by_id(attachment.id) == nil
181 assert {:ok, files} = File.ls(uploads_dir)
182 refute filename in files
183 end
184
185 test "with objects that have legacy data.url attribute" do
186 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
187 Pleroma.Config.put([:instance, :cleanup_attachments], true)
188
189 file = %Plug.Upload{
190 content_type: "image/jpg",
191 path: Path.absname("test/fixtures/image.jpg"),
192 filename: "an_image.jpg"
193 }
194
195 user = insert(:user)
196
197 {:ok, %Object{} = attachment} =
198 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
199
200 {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id})
201
202 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
203 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
204
205 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
206
207 path = href |> Path.dirname() |> Path.basename()
208
209 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
210
211 Object.delete(note)
212
213 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
214
215 assert Object.get_by_id(note.id).data["deleted"]
216 assert Object.get_by_id(attachment.id) == nil
217
218 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
219 end
220
221 test "With custom base_url" do
222 Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
223 Pleroma.Config.put([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/")
224 Pleroma.Config.put([:instance, :cleanup_attachments], true)
225
226 file = %Plug.Upload{
227 content_type: "image/jpg",
228 path: Path.absname("test/fixtures/image.jpg"),
229 filename: "an_image.jpg"
230 }
231
232 user = insert(:user)
233
234 {:ok, %Object{} = attachment} =
235 Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
236
237 %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} =
238 note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}})
239
240 uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
241
242 path = href |> Path.dirname() |> Path.basename()
243
244 assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}")
245
246 Object.delete(note)
247
248 ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker))
249
250 assert Object.get_by_id(note.id).data["deleted"]
251 assert Object.get_by_id(attachment.id) == nil
252
253 assert {:ok, []} == File.ls("#{uploads_dir}/#{path}")
254 end
255 end
256
257 describe "normalizer" do
258 test "fetches unknown objects by default" do
259 %Object{} =
260 object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367")
261
262 assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367"
263 end
264
265 test "fetches unknown objects when fetch_remote is explicitly true" do
266 %Object{} =
267 object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367", true)
268
269 assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367"
270 end
271
272 test "does not fetch unknown objects when fetch_remote is false" do
273 assert is_nil(
274 Object.normalize("http://mastodon.example.org/@admin/99541947525187367", false)
275 )
276 end
277 end
278
279 describe "get_by_id_and_maybe_refetch" do
280 setup do
281 mock(fn
282 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
283 %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
284
285 env ->
286 apply(HttpRequestMock, :request, [env])
287 end)
288
289 mock_modified = fn resp ->
290 mock(fn
291 %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
292 resp
293
294 env ->
295 apply(HttpRequestMock, :request, [env])
296 end)
297 end
298
299 on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
300
301 [mock_modified: mock_modified]
302 end
303
304 test "refetches if the time since the last refetch is greater than the interval", %{
305 mock_modified: mock_modified
306 } do
307 %Object{} =
308 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
309
310 Object.set_cache(object)
311
312 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
313 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
314
315 mock_modified.(%Tesla.Env{
316 status: 200,
317 body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
318 })
319
320 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
321 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
322 assert updated_object == object_in_cache
323 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
324 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
325 end
326
327 test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
328 %Object{} =
329 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
330
331 Object.set_cache(object)
332
333 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
334 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
335
336 assert capture_log(fn ->
337 mock_modified.(%Tesla.Env{status: 404, body: ""})
338
339 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
340 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
341 assert updated_object == object_in_cache
342 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
343 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
344 end) =~
345 "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
346 end
347
348 test "does not refetch if the time since the last refetch is greater than the interval", %{
349 mock_modified: mock_modified
350 } do
351 %Object{} =
352 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
353
354 Object.set_cache(object)
355
356 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
357 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
358
359 mock_modified.(%Tesla.Env{
360 status: 200,
361 body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
362 })
363
364 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
365 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
366 assert updated_object == object_in_cache
367 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
368 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
369 end
370
371 test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
372 %Object{} =
373 object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
374
375 Object.set_cache(object)
376
377 assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
378 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
379
380 user = insert(:user)
381 activity = Activity.get_create_by_object_ap_id(object.data["id"])
382 {:ok, _activity, object} = CommonAPI.favorite(activity.id, user)
383
384 assert object.data["like_count"] == 1
385
386 mock_modified.(%Tesla.Env{
387 status: 200,
388 body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
389 })
390
391 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
392 object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
393 assert updated_object == object_in_cache
394 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
395 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
396
397 assert updated_object.data["like_count"] == 1
398 end
399 end
400 end