Merge remote-tracking branch 'origin/feature/bounded-retention' into shigusegubu-new
This commit is contained in:
commit
d9e75192ad
13 changed files with 1340 additions and 42 deletions
|
|
@ -200,6 +200,58 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
refute Object.get_by_ap_id(old_remote_post2_id)
|
||||
end
|
||||
|
||||
test "with the --keep-threads option it keeps old threads a local user quoted", %{
|
||||
old_insert_date: old_insert_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
{:ok, quoted} = CommonAPI.post(remote_user, %{status: "quote me"})
|
||||
|
||||
quoted
|
||||
|> Ecto.Changeset.change(%{local: false, updated_at: old_insert_date})
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, quote} = CommonAPI.post(local_user, %{status: "look", quote_id: quoted.id})
|
||||
|
||||
quote
|
||||
|> Ecto.Changeset.change(%{updated_at: old_insert_date})
|
||||
|> Repo.update!()
|
||||
|
||||
Mix.Tasks.Pleroma.Database.run(["prune_objects", "--keep-threads"])
|
||||
|
||||
assert Object.get_by_ap_id(quoted.data["object"])
|
||||
end
|
||||
|
||||
test "with the --keep-threads option it keeps old threads addressed to a local user", %{
|
||||
old_insert_date: old_insert_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
{:ok, mention} = CommonAPI.post(remote_user, %{status: "hey @#{local_user.nickname}"})
|
||||
|
||||
{:ok, dm} =
|
||||
CommonAPI.post(remote_user, %{
|
||||
status: "psst @#{local_user.nickname}",
|
||||
visibility: "direct"
|
||||
})
|
||||
|
||||
# The pin must not depend on the notifications still being there
|
||||
Pleroma.Notification.clear(local_user)
|
||||
|
||||
for activity <- [mention, dm] do
|
||||
activity
|
||||
|> Ecto.Changeset.change(%{local: false, updated_at: old_insert_date})
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
Mix.Tasks.Pleroma.Database.run(["prune_objects", "--keep-threads"])
|
||||
|
||||
assert Object.get_by_ap_id(mention.data["object"])
|
||||
assert Object.get_by_ap_id(dm.data["object"])
|
||||
end
|
||||
|
||||
test "with the --keep-threads option it still keeps non-old threads even with no local interactions" do
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
|
|
@ -407,6 +459,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
["push_subscriptions"],
|
||||
["registrations"],
|
||||
["report_notes"],
|
||||
["retention_cursors"],
|
||||
["rich_media_card"],
|
||||
["rules"],
|
||||
["scheduled_activities"],
|
||||
|
|
|
|||
587
test/pleroma/retention_test.exs
Normal file
587
test/pleroma/retention_test.exs
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.RetentionTest do
|
||||
use Pleroma.DataCase, async: false
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Retention
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Bitwise
|
||||
import Ecto.Query
|
||||
import Pleroma.Factory
|
||||
|
||||
setup do
|
||||
clear_config([:instance, :remote_post_retention_days], 90)
|
||||
clear_config([:retention, :enabled], true)
|
||||
clear_config([:retention, :max_objects], nil)
|
||||
clear_config([:retention, :batch_size], 500)
|
||||
clear_config([:retention, :keep_non_public], false)
|
||||
|
||||
old_date =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(-200 * 86_400)
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|
||||
%{old_date: old_date}
|
||||
end
|
||||
|
||||
# Activity ids are time-ordered flakes and the retention walk goes by id, so
|
||||
# an "old" activity needs an old id as well as an old timestamp. Must be
|
||||
# applied before anything references the activity (bookmarks, notifications).
|
||||
defp age(%Activity{} = activity, old_date, changes) do
|
||||
ms = old_date |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_unix(:millisecond)
|
||||
|
||||
old_id =
|
||||
FlakeId.to_string(<<ms::integer-size(64), :rand.uniform(1 <<< 62)::integer-size(64)>>)
|
||||
|
||||
changes = Map.merge(%{id: old_id, updated_at: old_date}, changes)
|
||||
|
||||
{1, _} =
|
||||
Activity
|
||||
|> where([a], a.id == ^activity.id)
|
||||
|> Repo.update_all(set: Map.to_list(changes))
|
||||
|
||||
Map.merge(activity, changes)
|
||||
rescue
|
||||
e in Postgrex.Error ->
|
||||
reraise "age/3 must run before anything references the activity: #{Exception.message(e)}",
|
||||
__STACKTRACE__
|
||||
end
|
||||
|
||||
# Posts made through CommonAPI are local; turn the activity into a remote one
|
||||
# and backdate it so it looks like an old federated post.
|
||||
defp make_remote_and_old(activity, old_date), do: age(activity, old_date, %{local: false})
|
||||
|
||||
defp days_ago(days) do
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(-days * 86_400)
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
end
|
||||
|
||||
defp make_old(activity, old_date), do: age(activity, old_date, %{})
|
||||
|
||||
defp old_remote_post(user, old_date, params \\ %{}) do
|
||||
{:ok, activity} = CommonAPI.post(user, Map.merge(%{status: "some thing"}, params))
|
||||
make_remote_and_old(activity, old_date)
|
||||
end
|
||||
|
||||
describe "estimated_object_count/0" do
|
||||
test "falls back to an exact count when the planner has no statistics" do
|
||||
insert(:note)
|
||||
insert(:note)
|
||||
|
||||
assert Retention.estimated_object_count() == Repo.aggregate(Object, :count)
|
||||
end
|
||||
end
|
||||
|
||||
describe "run/1" do
|
||||
test "evicts an old remote thread nobody local touched", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
{:ok, reply} =
|
||||
CommonAPI.post(remote_user2, %{status: "reply", in_reply_to_status_id: post.id})
|
||||
|
||||
make_remote_and_old(reply, old_date)
|
||||
|
||||
{:ok, like} = CommonAPI.favorite(post.id, remote_user2)
|
||||
make_remote_and_old(like, old_date)
|
||||
|
||||
assert %{contexts: 1, objects: 2, activities: 3} = Retention.run()
|
||||
|
||||
refute Object.get_by_ap_id(post.data["object"])
|
||||
refute Object.get_by_ap_id(reply.data["object"])
|
||||
assert Repo.all(Activity) == []
|
||||
end
|
||||
|
||||
test "keeps recent remote threads" do
|
||||
remote_user = insert(:user, local: false)
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "fresh"})
|
||||
post |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
|
||||
assert %{contexts: 0, objects: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps old local posts", %{old_date: old_date} do
|
||||
user = insert(:user)
|
||||
{:ok, post} = CommonAPI.post(user, %{status: "mine"})
|
||||
make_old(post, old_date)
|
||||
|
||||
assert %{contexts: 0, objects: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote thread a local user favourited", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
{:ok, like} = CommonAPI.favorite(post.id, local_user)
|
||||
make_old(like, old_date)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote thread a local user replied to", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
{:ok, reply} =
|
||||
CommonAPI.post(local_user, %{status: "reply", in_reply_to_status_id: post.id})
|
||||
|
||||
make_old(reply, old_date)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote thread a local user bookmarked", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
{:ok, _} = Bookmark.create(local_user.id, post.id)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote post that mentioned a local user", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "hey @#{local_user.nickname}"})
|
||||
Repo.delete_all(Pleroma.Notification)
|
||||
post = make_remote_and_old(post, old_date)
|
||||
assert {:ok, [_]} = Pleroma.Notification.create_notifications(post)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps old remote posts addressed to a local user after notifications are cleared", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
{:ok, dm} =
|
||||
CommonAPI.post(remote_user, %{
|
||||
status: "psst @#{local_user.nickname}",
|
||||
visibility: "direct"
|
||||
})
|
||||
|
||||
{:ok, mention} = CommonAPI.post(remote_user, %{status: "hey @#{local_user.nickname}"})
|
||||
|
||||
Pleroma.Notification.clear(local_user)
|
||||
|
||||
for post <- [dm, mention] do
|
||||
make_remote_and_old(post, old_date)
|
||||
end
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(dm.data["object"])
|
||||
assert Object.get_by_ap_id(mention.data["object"])
|
||||
end
|
||||
|
||||
test "keeps a pinned post that an evicted thread's activity points at", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
{:ok, local_like} = CommonAPI.favorite(post.id, local_user)
|
||||
make_old(local_like, old_date)
|
||||
|
||||
# A remote like that arrived with a context of its own
|
||||
{:ok, like} = CommonAPI.favorite(post.id, remote_user2)
|
||||
other_context = "https://remote.example/contexts/#{Ecto.UUID.generate()}"
|
||||
age(like, old_date, %{local: false, data: Map.put(like.data, "context", other_context)})
|
||||
|
||||
assert %{contexts: 1, objects: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
assert Activity.get_by_id(post.id)
|
||||
end
|
||||
|
||||
test "keeps threads pinned by a local reaction of the legacy EmojiReaction type", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
Repo.insert!(%Activity{
|
||||
data: %{
|
||||
"id" => "#{Pleroma.Web.Endpoint.url()}/activities/#{Ecto.UUID.generate()}",
|
||||
"type" => "EmojiReaction",
|
||||
"actor" => local_user.ap_id,
|
||||
"object" => post.data["object"],
|
||||
"content" => "👍",
|
||||
"context" => post.data["context"]
|
||||
},
|
||||
local: true,
|
||||
actor: local_user.ap_id,
|
||||
recipients: [remote_user.ap_id]
|
||||
})
|
||||
|> make_old(old_date)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote thread whose post a local user quoted", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
quoted = old_remote_post(remote_user, old_date)
|
||||
{:ok, quote} = CommonAPI.post(local_user, %{status: "look at this", quote_id: quoted.id})
|
||||
make_old(quote, old_date)
|
||||
|
||||
# The quote has a context of its own and does not pin the quoted thread.
|
||||
refute quote.data["context"] == quoted.data["context"]
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(quoted.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old remote post that was reported", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
{:ok, _flag} =
|
||||
CommonAPI.report(local_user, %{account_id: remote_user.id, status_ids: [post.id]})
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "evicts an old remote thread even if remote users interacted with it", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
{:ok, announce} = CommonAPI.repeat(post.id, remote_user2)
|
||||
make_remote_and_old(announce, old_date)
|
||||
|
||||
assert %{contexts: 1, objects: 1, activities: 2} = Retention.run()
|
||||
refute Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "evicts old threads in batches, oldest first", %{old_date: old_date} do
|
||||
clear_config([:retention, :batch_size], 1)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
older = old_remote_post(remote_user, NaiveDateTime.add(old_date, -86_400))
|
||||
newer = old_remote_post(remote_user, old_date)
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(older.data["object"])
|
||||
assert Object.get_by_ap_id(newer.data["object"])
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(newer.data["object"])
|
||||
end
|
||||
|
||||
test "evicts threads younger than the retention period when over the object watermark" do
|
||||
clear_config([:retention, :max_objects], 1)
|
||||
clear_config([:retention, :batch_size], 1)
|
||||
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
older = old_remote_post(remote_user, days_ago(3))
|
||||
newer = old_remote_post(remote_user, days_ago(2))
|
||||
{:ok, mine} = CommonAPI.post(local_user, %{status: "mine"})
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(older.data["object"])
|
||||
assert Object.get_by_ap_id(newer.data["object"])
|
||||
assert Object.get_by_ap_id(mine.data["object"])
|
||||
end
|
||||
|
||||
test "keeps threads quiet for less than a day, even over the watermark" do
|
||||
clear_config([:retention, :max_objects], 0)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
post = old_remote_post(remote_user, NaiveDateTime.add(days_ago(1), 3_600))
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old thread with a recent reply, even over the watermark", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
clear_config([:retention, :max_objects], 0)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "old but active"})
|
||||
# Old id, so the walk reaches it, but active an hour ago
|
||||
age(post, old_date, %{local: false, updated_at: NaiveDateTime.add(days_ago(0), -3_600)})
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "does not evict anything below the watermark" do
|
||||
clear_config([:retention, :max_objects], 100)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "fresh"})
|
||||
post |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "with keep_non_public it keeps old non-public remote threads", %{old_date: old_date} do
|
||||
clear_config([:retention, :keep_non_public], true)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
{:ok, private} = CommonAPI.post(remote_user, %{status: "psst", visibility: "private"})
|
||||
make_remote_and_old(private, old_date)
|
||||
|
||||
public = old_remote_post(remote_user, old_date)
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
assert Object.get_by_ap_id(private.data["object"])
|
||||
refute Object.get_by_ap_id(public.data["object"])
|
||||
end
|
||||
|
||||
test "never touches activities without a context, even over the watermark" do
|
||||
clear_config([:retention, :max_objects], 1)
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
{:ok, _, _, follow} = CommonAPI.follow(local_user, remote_user)
|
||||
{:ok, block} = CommonAPI.block(remote_user2, local_user)
|
||||
|
||||
# Enough objects to trip the watermark
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "a"})
|
||||
post |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
{:ok, post2} = CommonAPI.post(remote_user, %{status: "b"})
|
||||
post2 |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
|
||||
for activity <- [follow, block] do
|
||||
assert is_nil(activity.data["context"])
|
||||
|
||||
activity
|
||||
|> Ecto.Changeset.change(%{local: false})
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
Retention.run()
|
||||
|
||||
assert Activity.get_by_id(follow.id)
|
||||
assert Activity.get_by_id(block.id)
|
||||
end
|
||||
|
||||
test "keeps remote reports and the threads they point at", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
reporter = insert(:user, local: false)
|
||||
admin = insert(:user, is_admin: true)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
flag =
|
||||
Repo.insert!(%Activity{
|
||||
data: %{
|
||||
"id" => "https://domain1.com/activities/#{Ecto.UUID.generate()}",
|
||||
"type" => "Flag",
|
||||
"actor" => reporter.ap_id,
|
||||
"object" => [remote_user.ap_id, post.data["object"]],
|
||||
"context" => "https://domain1.com/contexts/#{Ecto.UUID.generate()}",
|
||||
"content" => "spam"
|
||||
},
|
||||
local: false,
|
||||
actor: reporter.ap_id,
|
||||
recipients: []
|
||||
})
|
||||
|
||||
flag = make_old(flag, old_date)
|
||||
{:ok, _} = Pleroma.ReportNote.create(admin.id, flag.id, "looking into it")
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Activity.get_by_id(flag.id)
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "removes remote activities outside the thread that point at evicted objects", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
remote_user = insert(:user, local: false)
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
delete =
|
||||
Repo.insert!(%Activity{
|
||||
data: %{
|
||||
"id" => "https://domain1.com/activities/#{Ecto.UUID.generate()}",
|
||||
"type" => "Delete",
|
||||
"actor" => remote_user.ap_id,
|
||||
"object" => post.data["object"]
|
||||
},
|
||||
local: false,
|
||||
actor: remote_user.ap_id,
|
||||
recipients: []
|
||||
})
|
||||
|
||||
assert %{contexts: 1, objects: 1, activities: 2} = Retention.run()
|
||||
refute Activity.get_by_id(delete.id)
|
||||
end
|
||||
|
||||
test "never deletes local objects, even inside an otherwise remote thread", %{
|
||||
old_date: old_date
|
||||
} do
|
||||
local_user = insert(:user)
|
||||
{:ok, post} = CommonAPI.post(local_user, %{status: "mine, honestly"})
|
||||
# Pretend the Create activity federated in from elsewhere, addressed to
|
||||
# nobody local (the local followers collection would pin the thread)
|
||||
age(post, old_date, %{local: false, recipients: []})
|
||||
|
||||
assert %{contexts: 1, objects: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "remembers where it got to and does not rescan", %{old_date: old_date} do
|
||||
clear_config([:retention, :batch_size], 1)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
assert is_nil(Retention.Cursor.get("activities"))
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
assert Retention.Cursor.get("activities") == post.id
|
||||
|
||||
# An older activity that appears behind the cursor is not visited again
|
||||
# until the cursor is reset.
|
||||
older = old_remote_post(remote_user, NaiveDateTime.add(old_date, -86_400))
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(older.data["object"])
|
||||
|
||||
Retention.Cursor.reset("activities")
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(older.data["object"])
|
||||
end
|
||||
|
||||
test "keeps an old thread that is still active", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
remote_user2 = insert(:user, local: false)
|
||||
|
||||
post = old_remote_post(remote_user, old_date)
|
||||
|
||||
{:ok, reply} =
|
||||
CommonAPI.post(remote_user2, %{status: "still here", in_reply_to_status_id: post.id})
|
||||
|
||||
reply |> Ecto.Changeset.change(%{local: false}) |> Repo.update!()
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
|
||||
test "keeps threads pinned by each local interaction type", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
liked = old_remote_post(remote_user, old_date)
|
||||
{:ok, like} = CommonAPI.favorite(liked.id, local_user)
|
||||
make_old(like, old_date)
|
||||
|
||||
repeated = old_remote_post(remote_user, old_date)
|
||||
{:ok, announce} = CommonAPI.repeat(repeated.id, local_user)
|
||||
make_old(announce, old_date)
|
||||
|
||||
reacted = old_remote_post(remote_user, old_date)
|
||||
{:ok, react} = CommonAPI.react_with_emoji(reacted.id, local_user, "👍")
|
||||
make_old(react, old_date)
|
||||
|
||||
replied = old_remote_post(remote_user, old_date)
|
||||
{:ok, reply} = CommonAPI.post(local_user, %{status: "r", in_reply_to_status_id: replied.id})
|
||||
make_old(reply, old_date)
|
||||
|
||||
assert %{contexts: 0} = Retention.run()
|
||||
|
||||
for post <- [liked, repeated, reacted, replied] do
|
||||
assert Object.get_by_ap_id(post.data["object"])
|
||||
end
|
||||
end
|
||||
|
||||
test "a watermark run does not stall the age-based walk", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
clear_config([:retention, :max_objects], 0)
|
||||
recent = old_remote_post(remote_user, days_ago(2))
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(recent.data["object"])
|
||||
|
||||
clear_config([:retention, :max_objects], nil)
|
||||
old = old_remote_post(remote_user, old_date)
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
refute Object.get_by_ap_id(old.data["object"])
|
||||
end
|
||||
|
||||
test "moves the cursor to the deadline once everything old is done", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
old_remote_post(remote_user, old_date)
|
||||
|
||||
Retention.run()
|
||||
|
||||
<<cursor_ms::integer-size(64), _::integer-size(64)>> =
|
||||
FlakeId.from_string(Retention.Cursor.get("activities"))
|
||||
|
||||
deadline_ms =
|
||||
Retention.deadline()
|
||||
|> DateTime.from_naive!("Etc/UTC")
|
||||
|> DateTime.to_unix(:millisecond)
|
||||
|
||||
assert_in_delta cursor_ms, deadline_ms, 5_000
|
||||
end
|
||||
|
||||
test "removes hashtags that no longer have objects", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
old_remote_post(remote_user, old_date, %{status: "#lonelytag"})
|
||||
assert Pleroma.Hashtag.get_by_name("lonelytag")
|
||||
|
||||
Retention.run()
|
||||
|
||||
refute Pleroma.Hashtag.get_by_name("lonelytag")
|
||||
end
|
||||
|
||||
test "only cleans up hashtags of the evicted objects", %{old_date: old_date} do
|
||||
remote_user = insert(:user, local: false)
|
||||
local_user = insert(:user)
|
||||
|
||||
old_remote_post(remote_user, old_date, %{status: "#gone #shared #followed"})
|
||||
{:ok, _} = CommonAPI.post(local_user, %{status: "#shared"})
|
||||
{:ok, followed} = Pleroma.Hashtag.get_or_create_by_name("followed")
|
||||
{:ok, _} = Pleroma.User.follow_hashtag(local_user, followed)
|
||||
{:ok, _} = Pleroma.Hashtag.get_or_create_by_name("unrelated")
|
||||
|
||||
assert %{contexts: 1} = Retention.run()
|
||||
|
||||
refute Pleroma.Hashtag.get_by_name("gone")
|
||||
assert Pleroma.Hashtag.get_by_name("shared")
|
||||
assert Pleroma.Hashtag.get_by_name("followed")
|
||||
assert Pleroma.Hashtag.get_by_name("unrelated")
|
||||
end
|
||||
end
|
||||
end
|
||||
52
test/pleroma/workers/cron/retention_worker_test.exs
Normal file
52
test/pleroma/workers/cron/retention_worker_test.exs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.Cron.RetentionWorkerTest do
|
||||
use Pleroma.DataCase, async: false
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Workers.Cron.RetentionWorker
|
||||
|
||||
import Ecto.Query
|
||||
import Pleroma.Factory
|
||||
|
||||
setup do
|
||||
clear_config([:instance, :remote_post_retention_days], 90)
|
||||
|
||||
remote_user = insert(:user, local: false)
|
||||
{:ok, post} = CommonAPI.post(remote_user, %{status: "old"})
|
||||
|
||||
old_date =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(-200 * 86_400)
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|
||||
ms = old_date |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_unix(:millisecond)
|
||||
old_id = FlakeId.to_string(<<ms::integer-size(64), 1::integer-size(64)>>)
|
||||
|
||||
{1, _} =
|
||||
Pleroma.Activity
|
||||
|> where([a], a.id == ^post.id)
|
||||
|> Repo.update_all(set: [id: old_id, local: false, updated_at: old_date])
|
||||
|
||||
%{object_id: post.data["object"]}
|
||||
end
|
||||
|
||||
test "does nothing when disabled", %{object_id: object_id} do
|
||||
clear_config([:retention, :enabled], false)
|
||||
|
||||
assert :ok = RetentionWorker.perform(%Oban.Job{})
|
||||
assert Object.get_by_ap_id(object_id)
|
||||
end
|
||||
|
||||
test "evicts when enabled", %{object_id: object_id} do
|
||||
clear_config([:retention, :enabled], true)
|
||||
clear_config([:retention, :batch_size], 500)
|
||||
|
||||
assert :ok = RetentionWorker.perform(%Oban.Job{})
|
||||
refute Object.get_by_ap_id(object_id)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue