Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/emojireactv...
[akkoma] / test / web / activity_pub / activity_pub_test.exs
index 53176917e988632a827add7f6eba7480bde85fbb..0739cbfef025421e902fbbd8fcf429036ecab33d 100644 (file)
@@ -16,11 +16,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.AdminAPI.AccountView
   alias Pleroma.Web.CommonAPI
-  alias Pleroma.Web.Federator
 
+  import ExUnit.CaptureLog
+  import Mock
   import Pleroma.Factory
   import Tesla.Mock
-  import Mock
 
   setup do
     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -873,187 +873,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     end
   end
 
-  describe "react to an object" do
-    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
-      Config.put([:instance, :federating], true)
-      user = insert(:user)
-      reactor = insert(:user)
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
-      assert object = Object.normalize(activity)
-
-      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
-
-      assert called(Federator.publish(reaction_activity))
-    end
-
-    test "adds an emoji reaction activity to the db" do
-      user = insert(:user)
-      reactor = insert(:user)
-      third_user = insert(:user)
-      fourth_user = insert(:user)
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
-      assert object = Object.normalize(activity)
-
-      {:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
-
-      assert reaction_activity
-
-      assert reaction_activity.data["actor"] == reactor.ap_id
-      assert reaction_activity.data["type"] == "EmojiReact"
-      assert reaction_activity.data["content"] == "🔥"
-      assert reaction_activity.data["object"] == object.data["id"]
-      assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
-      assert reaction_activity.data["context"] == object.data["context"]
-      assert object.data["reaction_count"] == 1
-      assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
-
-      {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕")
-
-      assert object.data["reaction_count"] == 2
-      assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]]
-
-      {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
-
-      assert object.data["reaction_count"] == 3
-
-      assert object.data["reactions"] == [
-               ["🔥", [fourth_user.ap_id, reactor.ap_id]],
-               ["☕", [third_user.ap_id]]
-             ]
-    end
-
-    test "reverts emoji reaction on error" do
-      [user, reactor] = insert_list(2, :user)
-
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
-      object = Object.normalize(activity)
-
-      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
-        assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
-      end
-
-      object = Object.get_by_ap_id(object.data["id"])
-      refute object.data["reaction_count"]
-      refute object.data["reactions"]
-    end
-  end
-
-  describe "unreacting to an object" do
-    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
-      Config.put([:instance, :federating], true)
-      user = insert(:user)
-      reactor = insert(:user)
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
-      assert object = Object.normalize(activity)
-
-      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
-
-      assert called(Federator.publish(reaction_activity))
-
-      {:ok, unreaction_activity, _object} =
-        ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
-
-      assert called(Federator.publish(unreaction_activity))
-    end
-
-    test "adds an undo activity to the db" do
-      user = insert(:user)
-      reactor = insert(:user)
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
-      assert object = Object.normalize(activity)
-
-      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
-
-      {:ok, unreaction_activity, _object} =
-        ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
-
-      assert unreaction_activity.actor == reactor.ap_id
-      assert unreaction_activity.data["object"] == reaction_activity.data["id"]
-
-      object = Object.get_by_ap_id(object.data["id"])
-      assert object.data["reaction_count"] == 0
-      assert object.data["reactions"] == []
-    end
-
-    test "reverts emoji unreact on error" do
-      [user, reactor] = insert_list(2, :user)
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
-      object = Object.normalize(activity)
-
-      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀")
-
-      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
-        assert {:error, :reverted} =
-                 ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
-      end
-
-      object = Object.get_by_ap_id(object.data["id"])
-
-      assert object.data["reaction_count"] == 1
-      assert object.data["reactions"] == [["😀", [reactor.ap_id]]]
-    end
-  end
-
-  describe "unliking" do
-    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
-      Config.put([:instance, :federating], true)
-
-      note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
-      user = insert(:user)
-
-      {:ok, object} = ActivityPub.unlike(user, object)
-      refute called(Federator.publish())
-
-      {:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id)
-      object = Object.get_by_id(object.id)
-      assert object.data["like_count"] == 1
-
-      {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
-      assert object.data["like_count"] == 0
-
-      assert called(Federator.publish(unlike_activity))
-    end
-
-    test "reverts unliking on error" do
-      note_activity = insert(:note_activity)
-      user = insert(:user)
-
-      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
-      object = Object.normalize(note_activity)
-      assert object.data["like_count"] == 1
-
-      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
-        assert {:error, :reverted} = ActivityPub.unlike(user, object)
-      end
-
-      assert Object.get_by_ap_id(object.data["id"]) == object
-      assert object.data["like_count"] == 1
-      assert Activity.get_by_id(like_activity.id)
-    end
-
-    test "unliking a previously liked object" do
-      note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
-      user = insert(:user)
-
-      # Unliking something that hasn't been liked does nothing
-      {:ok, object} = ActivityPub.unlike(user, object)
-      assert object.data["like_count"] == 0
-
-      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
-
-      object = Object.get_by_id(object.id)
-      assert object.data["like_count"] == 1
-
-      {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
-      assert object.data["like_count"] == 0
-
-      assert Activity.get_by_id(like_activity.id) == nil
-      assert note_activity.actor in unlike_activity.recipients
-    end
-  end
-
   describe "announcing an object" do
     test "adds an announce activity to the db" do
       note_activity = insert(:note_activity)
@@ -1123,52 +942,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     end
   end
 
-  describe "unannouncing an object" do
-    test "unannouncing a previously announced object" do
-      note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
-      user = insert(:user)
-
-      # Unannouncing an object that is not announced does nothing
-      {:ok, object} = ActivityPub.unannounce(user, object)
-      refute object.data["announcement_count"]
-
-      {:ok, announce_activity, object} = ActivityPub.announce(user, object)
-      assert object.data["announcement_count"] == 1
-
-      {:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
-      assert object.data["announcement_count"] == 0
-
-      assert unannounce_activity.data["to"] == [
-               User.ap_followers(user),
-               object.data["actor"]
-             ]
-
-      assert unannounce_activity.data["type"] == "Undo"
-      assert unannounce_activity.data["object"] == announce_activity.data
-      assert unannounce_activity.data["actor"] == user.ap_id
-      assert unannounce_activity.data["context"] == announce_activity.data["context"]
-
-      assert Activity.get_by_id(announce_activity.id) == nil
-    end
-
-    test "reverts unannouncing on error" do
-      note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
-      user = insert(:user)
-
-      {:ok, _announce_activity, object} = ActivityPub.announce(user, object)
-      assert object.data["announcement_count"] == 1
-
-      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
-        assert {:error, :reverted} = ActivityPub.unannounce(user, object)
-      end
-
-      object = Object.get_by_ap_id(object.data["id"])
-      assert object.data["announcement_count"] == 1
-    end
-  end
-
   describe "uploading files" do
     test "copies the file to the configured folder" do
       file = %Plug.Upload{
@@ -1275,7 +1048,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     end
   end
 
-  describe "blocking / unblocking" do
+  describe "blocking" do
     test "reverts block activity on error" do
       [blocker, blocked] = insert_list(2, :user)
 
@@ -1297,175 +1070,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert activity.data["actor"] == blocker.ap_id
       assert activity.data["object"] == blocked.ap_id
     end
-
-    test "reverts unblock activity on error" do
-      [blocker, blocked] = insert_list(2, :user)
-      {:ok, block_activity} = ActivityPub.block(blocker, blocked)
-
-      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
-        assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
-      end
-
-      assert block_activity.data["type"] == "Block"
-      assert block_activity.data["actor"] == blocker.ap_id
-
-      assert Repo.aggregate(Activity, :count, :id) == 1
-      assert Repo.aggregate(Object, :count, :id) == 1
-    end
-
-    test "creates an undo activity for the last block" do
-      blocker = insert(:user)
-      blocked = insert(:user)
-
-      {:ok, block_activity} = ActivityPub.block(blocker, blocked)
-      {:ok, activity} = ActivityPub.unblock(blocker, blocked)
-
-      assert activity.data["type"] == "Undo"
-      assert activity.data["actor"] == blocker.ap_id
-
-      embedded_object = activity.data["object"]
-      assert is_map(embedded_object)
-      assert embedded_object["type"] == "Block"
-      assert embedded_object["object"] == blocked.ap_id
-      assert embedded_object["id"] == block_activity.data["id"]
-    end
-  end
-
-  describe "deletion" do
-    setup do: clear_config([:instance, :rewrite_policy])
-
-    test "it reverts deletion on error" do
-      note = insert(:note_activity)
-      object = Object.normalize(note)
-
-      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
-        assert {:error, :reverted} = ActivityPub.delete(object)
-      end
-
-      assert Repo.aggregate(Activity, :count, :id) == 1
-      assert Repo.get(Object, object.id) == object
-      assert Activity.get_by_id(note.id) == note
-    end
-
-    test "it creates a delete activity and deletes the original object" do
-      note = insert(:note_activity)
-      object = Object.normalize(note)
-      {:ok, delete} = ActivityPub.delete(object)
-
-      assert delete.data["type"] == "Delete"
-      assert delete.data["actor"] == note.data["actor"]
-      assert delete.data["object"] == object.data["id"]
-
-      assert Activity.get_by_id(delete.id) != nil
-
-      assert Repo.get(Object, object.id).data["type"] == "Tombstone"
-    end
-
-    test "it doesn't fail when an activity was already deleted" do
-      {:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete()
-
-      assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete()
-    end
-
-    test "decrements user note count only for public activities" do
-      user = insert(:user, note_count: 10)
-
-      {:ok, a1} =
-        CommonAPI.post(User.get_cached_by_id(user.id), %{
-          "status" => "yeah",
-          "visibility" => "public"
-        })
-
-      {:ok, a2} =
-        CommonAPI.post(User.get_cached_by_id(user.id), %{
-          "status" => "yeah",
-          "visibility" => "unlisted"
-        })
-
-      {:ok, a3} =
-        CommonAPI.post(User.get_cached_by_id(user.id), %{
-          "status" => "yeah",
-          "visibility" => "private"
-        })
-
-      {:ok, a4} =
-        CommonAPI.post(User.get_cached_by_id(user.id), %{
-          "status" => "yeah",
-          "visibility" => "direct"
-        })
-
-      {:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
-      {:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
-      {:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
-      {:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
-
-      user = User.get_cached_by_id(user.id)
-      assert user.note_count == 10
-    end
-
-    test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
-      user = insert(:user)
-      note = insert(:note_activity)
-      object = Object.normalize(note)
-
-      {:ok, object} =
-        object
-        |> Object.change(%{
-          data: %{
-            "actor" => object.data["actor"],
-            "id" => object.data["id"],
-            "to" => [user.ap_id],
-            "type" => "Note"
-          }
-        })
-        |> Object.update_and_set_cache()
-
-      {:ok, delete} = ActivityPub.delete(object)
-
-      assert user.ap_id in delete.data["to"]
-    end
-
-    test "decreases reply count" do
-      user = insert(:user)
-      user2 = insert(:user)
-
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
-      reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
-      ap_id = activity.data["id"]
-
-      {:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
-      {:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
-      {:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
-      {:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
-
-      _ = CommonAPI.delete(direct_reply.id, user2)
-      assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-      assert object.data["repliesCount"] == 2
-
-      _ = CommonAPI.delete(private_reply.id, user2)
-      assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-      assert object.data["repliesCount"] == 2
-
-      _ = CommonAPI.delete(public_reply.id, user2)
-      assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-      assert object.data["repliesCount"] == 1
-
-      _ = CommonAPI.delete(unlisted_reply.id, user2)
-      assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-      assert object.data["repliesCount"] == 0
-    end
-
-    test "it passes delete activity through MRF before deleting the object" do
-      Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy)
-
-      note = insert(:note_activity)
-      object = Object.normalize(note)
-
-      {:error, {:reject, _}} = ActivityPub.delete(object)
-
-      assert Activity.get_by_id(note.id)
-      assert Repo.get(Object, object.id).data["type"] == object.data["type"]
-    end
   end
 
   describe "timeline post-processing" do
@@ -1910,4 +1514,544 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
                ActivityPub.move(old_user, new_user)
     end
   end
+
+  test "doesn't retrieve replies activities with exclude_replies" do
+    user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
+
+    {:ok, _reply} =
+      CommonAPI.post(user, %{"status" => "yeah", "in_reply_to_status_id" => activity.id})
+
+    [result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"})
+
+    assert result.id == activity.id
+
+    assert length(ActivityPub.fetch_public_activities()) == 2
+  end
+
+  describe "replies filtering with public messages" do
+    setup :public_messages
+
+    test "public timeline", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_filtering_user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 16
+    end
+
+    test "public timeline with reply_visibility `following`", %{
+      users: %{u1: user},
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4,
+      activities: activities
+    } do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("reply_filtering_user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 14
+
+      visible_ids =
+        Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "public timeline with reply_visibility `self`", %{
+      users: %{u1: user},
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4,
+      activities: activities
+    } do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("reply_filtering_user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 10
+      visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities)
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 13
+
+      visible_ids =
+        Map.values(u1) ++
+          Map.values(u3) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u2[:r3],
+            u4[:r1],
+            u4[:r2]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline with reply_visibility `following`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 11
+
+      visible_ids =
+        Map.values(u1) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u2[:r3],
+            u3[:r1],
+            u4[:r1],
+            u4[:r2]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline with reply_visibility `self`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 9
+
+      visible_ids =
+        Map.values(u1) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u3[:r1],
+            u4[:r1]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+  end
+
+  describe "replies filtering with private messages" do
+    setup :private_messages
+
+    test "public timeline", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("reply_filtering_user", user)
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("reply_filtering_user", user)
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "home timeline", %{users: %{u1: user}} do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 12
+    end
+
+    test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 12
+    end
+
+    test "home timeline with default reply_visibility `self`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 10
+
+      visible_ids =
+        Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities)
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+  end
+
+  defp public_messages(_) do
+    [u1, u2, u3, u4] = insert_list(4, :user)
+    {:ok, u1} = User.follow(u1, u2)
+    {:ok, u2} = User.follow(u2, u1)
+    {:ok, u1} = User.follow(u1, u4)
+    {:ok, u4} = User.follow(u4, u1)
+
+    {:ok, u2} = User.follow(u2, u3)
+    {:ok, u3} = User.follow(u3, u2)
+
+    {:ok, a1} = CommonAPI.post(u1, %{"status" => "Status"})
+
+    {:ok, r1_1} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u1.nickname} reply from u2 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, r1_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u1.nickname} reply from u3 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, r1_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u1.nickname} reply from u4 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, a2} = CommonAPI.post(u2, %{"status" => "Status"})
+
+    {:ok, r2_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u2.nickname} reply from u1 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, r2_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u2.nickname} reply from u3 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, r2_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u2.nickname} reply from u4 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, a3} = CommonAPI.post(u3, %{"status" => "Status"})
+
+    {:ok, r3_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u3.nickname} reply from u1 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, r3_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u3.nickname} reply from u2 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, r3_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u3.nickname} reply from u4 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, a4} = CommonAPI.post(u4, %{"status" => "Status"})
+
+    {:ok, r4_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u4.nickname} reply from u1 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok, r4_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u4.nickname} reply from u2 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok, r4_3} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u4.nickname} reply from u3 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok,
+     users: %{u1: u1, u2: u2, u3: u3, u4: u4},
+     activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
+     u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
+     u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id},
+     u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id},
+     u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}}
+  end
+
+  defp private_messages(_) do
+    [u1, u2, u3, u4] = insert_list(4, :user)
+    {:ok, u1} = User.follow(u1, u2)
+    {:ok, u2} = User.follow(u2, u1)
+    {:ok, u1} = User.follow(u1, u3)
+    {:ok, u3} = User.follow(u3, u1)
+    {:ok, u1} = User.follow(u1, u4)
+    {:ok, u4} = User.follow(u4, u1)
+
+    {:ok, u2} = User.follow(u2, u3)
+    {:ok, u3} = User.follow(u3, u2)
+
+    {:ok, a1} = CommonAPI.post(u1, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r1_1} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u1.nickname} reply from u2 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r1_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u1.nickname} reply from u3 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r1_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u1.nickname} reply from u4 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a2} = CommonAPI.post(u2, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r2_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u2.nickname} reply from u1 to u2",
+        "in_reply_to_status_id" => a2.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r2_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u2.nickname} reply from u3 to u2",
+        "in_reply_to_status_id" => a2.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a3} = CommonAPI.post(u3, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r3_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u3.nickname} reply from u1 to u3",
+        "in_reply_to_status_id" => a3.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r3_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u3.nickname} reply from u2 to u3",
+        "in_reply_to_status_id" => a3.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a4} = CommonAPI.post(u4, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r4_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u4.nickname} reply from u1 to u4",
+        "in_reply_to_status_id" => a4.id,
+        "visibility" => "private"
+      })
+
+    {:ok,
+     users: %{u1: u1, u2: u2, u3: u3, u4: u4},
+     activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
+     u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
+     u2: %{r1: r2_1.id, r2: r2_2.id},
+     u3: %{r1: r3_1.id, r2: r3_2.id},
+     u4: %{r1: r4_1.id}}
+  end
+
+  describe "maybe_update_follow_information/1" do
+    setup do
+      clear_config([:instance, :external_user_synchronization], true)
+
+      user = %{
+        local: false,
+        ap_id: "https://gensokyo.2hu/users/raymoo",
+        following_address: "https://gensokyo.2hu/users/following",
+        follower_address: "https://gensokyo.2hu/users/followers",
+        type: "Person"
+      }
+
+      %{user: user}
+    end
+
+    test "logs an error when it can't fetch the info", %{user: user} do
+      assert capture_log(fn ->
+               ActivityPub.maybe_update_follow_information(user)
+             end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+    end
+
+    test "just returns the input if the user type is Application", %{
+      user: user
+    } do
+      user =
+        user
+        |> Map.put(:type, "Application")
+
+      refute capture_log(fn ->
+               assert ^user = ActivityPub.maybe_update_follow_information(user)
+             end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+    end
+
+    test "it just returns the input if the user has no following/follower addresses", %{
+      user: user
+    } do
+      user =
+        user
+        |> Map.put(:following_address, nil)
+        |> Map.put(:follower_address, nil)
+
+      refute capture_log(fn ->
+               assert ^user = ActivityPub.maybe_update_follow_information(user)
+             end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+    end
+  end
 end