Merge remote-tracking branch 'remotes/origin/develop' into 1505-threads-federation
[akkoma] / test / web / activity_pub / transmogrifier_test.exs
index 76e7ad8ea2036eade1c6f1e2a5ec5278e8201ec1..937f78cbeddcaf6adadf43dee00d335bd8f3e1da 100644 (file)
@@ -3,7 +3,9 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
+  use Oban.Testing, repo: Pleroma.Repo
   use Pleroma.DataCase
   use Pleroma.DataCase
+
   alias Pleroma.Activity
   alias Pleroma.Object
   alias Pleroma.Object.Fetcher
   alias Pleroma.Activity
   alias Pleroma.Object
   alias Pleroma.Object.Fetcher
@@ -11,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Transmogrifier
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Transmogrifier
+  alias Pleroma.Web.AdminAPI.AccountView
   alias Pleroma.Web.CommonAPI
 
   import Mock
   alias Pleroma.Web.CommonAPI
 
   import Mock
@@ -38,7 +41,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert activity == returned_activity
     end
 
       assert activity == returned_activity
     end
 
-    test "it fetches replied-to activities if we don't have them" do
+    @tag capture_log: true
+    test "it fetches reply-to activities if we don't have them" do
       data =
         File.read!("test/fixtures/mastodon-post-activity.json")
         |> Poison.decode!()
       data =
         File.read!("test/fixtures/mastodon-post-activity.json")
         |> Poison.decode!()
@@ -59,7 +63,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
     end
 
       assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
     end
 
-    test "it does not fetch replied-to activities beyond max_replies_depth" do
+    test "it does not fetch reply-to activities beyond max replies depth limit" do
       data =
         File.read!("test/fixtures/mastodon-post-activity.json")
         |> Poison.decode!()
       data =
         File.read!("test/fixtures/mastodon-post-activity.json")
         |> Poison.decode!()
@@ -71,7 +75,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       data = Map.put(data, "object", object)
 
       with_mock Pleroma.Web.Federator,
       data = Map.put(data, "object", object)
 
       with_mock Pleroma.Web.Federator,
-        allowed_incoming_reply_depth?: fn _ -> false end do
+        allowed_thread_distance?: fn _ -> false end do
         {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
 
         returned_object = Object.normalize(returned_activity, false)
         {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
 
         returned_object = Object.normalize(returned_activity, false)
@@ -338,6 +342,99 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert data["object"] == activity.data["object"]
     end
 
       assert data["object"] == activity.data["object"]
     end
 
+    test "it works for incoming misskey likes, turning them into EmojiReacts" do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+      data =
+        File.read!("test/fixtures/misskey-like.json")
+        |> Poison.decode!()
+        |> Map.put("object", activity.data["object"])
+
+      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+      assert data["actor"] == data["actor"]
+      assert data["type"] == "EmojiReact"
+      assert data["id"] == data["id"]
+      assert data["object"] == activity.data["object"]
+      assert data["content"] == "🍮"
+    end
+
+    test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+      data =
+        File.read!("test/fixtures/misskey-like.json")
+        |> Poison.decode!()
+        |> Map.put("object", activity.data["object"])
+        |> Map.put("_misskey_reaction", "⭐")
+
+      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+      assert data["actor"] == data["actor"]
+      assert data["type"] == "EmojiReact"
+      assert data["id"] == data["id"]
+      assert data["object"] == activity.data["object"]
+      assert data["content"] == "⭐"
+    end
+
+    test "it works for incoming emoji reactions" do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+      data =
+        File.read!("test/fixtures/emoji-reaction.json")
+        |> Poison.decode!()
+        |> Map.put("object", activity.data["object"])
+
+      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+      assert data["actor"] == "http://mastodon.example.org/users/admin"
+      assert data["type"] == "EmojiReact"
+      assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
+      assert data["object"] == activity.data["object"]
+      assert data["content"] == "👌"
+    end
+
+    test "it reject invalid emoji reactions" do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+
+      data =
+        File.read!("test/fixtures/emoji-reaction-too-long.json")
+        |> Poison.decode!()
+        |> Map.put("object", activity.data["object"])
+
+      assert :error = Transmogrifier.handle_incoming(data)
+
+      data =
+        File.read!("test/fixtures/emoji-reaction-no-emoji.json")
+        |> Poison.decode!()
+        |> Map.put("object", activity.data["object"])
+
+      assert :error = Transmogrifier.handle_incoming(data)
+    end
+
+    test "it works for incoming emoji reaction undos" do
+      user = insert(:user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+      {:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
+
+      data =
+        File.read!("test/fixtures/mastodon-undo-like.json")
+        |> Poison.decode!()
+        |> Map.put("object", reaction_activity.data["id"])
+        |> Map.put("actor", user.ap_id)
+
+      {:ok, activity} = Transmogrifier.handle_incoming(data)
+
+      assert activity.actor == user.ap_id
+      assert activity.data["id"] == data["id"]
+      assert activity.data["type"] == "Undo"
+    end
+
     test "it returns an error for incoming unlikes wihout a like activity" do
       user = insert(:user)
       {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
     test "it returns an error for incoming unlikes wihout a like activity" do
       user = insert(:user)
       {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
@@ -458,6 +555,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert object.data["content"] == "this is a private toot"
     end
 
       assert object.data["content"] == "this is a private toot"
     end
 
+    @tag capture_log: true
     test "it rejects incoming announces with an inlined activity from another origin" do
       data =
         File.read!("test/fixtures/bogus-mastodon-announce.json")
     test "it rejects incoming announces with an inlined activity from another origin" do
       data =
         File.read!("test/fixtures/bogus-mastodon-announce.json")
@@ -552,6 +650,20 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       refute Map.has_key?(object.data, "likes")
     end
 
       refute Map.has_key?(object.data, "likes")
     end
 
+    test "it strips internal reactions" do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
+      {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
+
+      %{object: object} = Activity.get_by_id_with_object(activity.id)
+      assert Map.has_key?(object.data, "reactions")
+      assert Map.has_key?(object.data, "reaction_count")
+
+      object_data = Transmogrifier.strip_internal_fields(object.data)
+      refute Map.has_key?(object_data, "reactions")
+      refute Map.has_key?(object_data, "reaction_count")
+    end
+
     test "it works for incoming update activities" do
       data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
 
     test "it works for incoming update activities" do
       data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
 
@@ -756,6 +868,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert Activity.get_by_id(activity.id)
     end
 
       assert Activity.get_by_id(activity.id)
     end
 
+    @tag capture_log: true
     test "it works for incoming user deletes" do
       %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
 
     test "it works for incoming user deletes" do
       %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
 
@@ -777,7 +890,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
         |> Poison.decode!()
         |> Map.put("actor", ap_id)
 
         |> Poison.decode!()
         |> Map.put("actor", ap_id)
 
-      assert :error == Transmogrifier.handle_incoming(data)
+      assert capture_log(fn ->
+               assert :error == Transmogrifier.handle_incoming(data)
+             end) =~ "Object containment failed"
+
       assert User.get_cached_by_ap_id(ap_id)
     end
 
       assert User.get_cached_by_ap_id(ap_id)
     end
 
@@ -835,6 +951,25 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
     end
 
       refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
     end
 
+    test "it works for incoming follows to locked account" do
+      pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
+      user = insert(:user, locked: true)
+
+      data =
+        File.read!("test/fixtures/mastodon-follow-activity.json")
+        |> Poison.decode!()
+        |> Map.put("object", user.ap_id)
+
+      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+      assert data["type"] == "Follow"
+      assert data["object"] == user.ap_id
+      assert data["state"] == "pending"
+      assert data["actor"] == "http://mastodon.example.org/users/admin"
+
+      assert [^pending_follower] = User.get_follow_requests(user)
+    end
+
     test "it works for incoming blocks" do
       user = insert(:user)
 
     test "it works for incoming blocks" do
       user = insert(:user)
 
@@ -1121,10 +1256,18 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
       object = Object.normalize(activity)
 
       {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
       object = Object.normalize(activity)
 
+      note_obj = %{
+        "type" => "Note",
+        "id" => activity.data["id"],
+        "content" => "test post",
+        "published" => object.data["published"],
+        "actor" => AccountView.render("show.json", %{user: user})
+      }
+
       message = %{
         "@context" => "https://www.w3.org/ns/activitystreams",
         "cc" => [user.ap_id],
       message = %{
         "@context" => "https://www.w3.org/ns/activitystreams",
         "cc" => [user.ap_id],
-        "object" => [user.ap_id, object.data["id"]],
+        "object" => [user.ap_id, activity.data["id"]],
         "type" => "Flag",
         "content" => "blocked AND reported!!!",
         "actor" => other_user.ap_id
         "type" => "Flag",
         "content" => "blocked AND reported!!!",
         "actor" => other_user.ap_id
@@ -1132,7 +1275,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
 
       assert {:ok, activity} = Transmogrifier.handle_incoming(message)
 
 
       assert {:ok, activity} = Transmogrifier.handle_incoming(message)
 
-      assert activity.data["object"] == [user.ap_id, object.data["id"]]
+      assert activity.data["object"] == [user.ap_id, note_obj]
       assert activity.data["content"] == "blocked AND reported!!!"
       assert activity.data["actor"] == other_user.ap_id
       assert activity.data["cc"] == [user.ap_id]
       assert activity.data["content"] == "blocked AND reported!!!"
       assert activity.data["actor"] == other_user.ap_id
       assert activity.data["cc"] == [user.ap_id]
@@ -1207,6 +1350,101 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
     end
   end
 
     end
   end
 
+  describe "`handle_incoming/2`, Mastodon format `replies` handling" do
+    clear_config([:activitypub, :note_replies_output_limit]) do
+      Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
+    end
+
+    clear_config([:instance, :federation_incoming_replies_max_depth])
+
+    setup do
+      data =
+        "test/fixtures/mastodon-post-activity.json"
+        |> File.read!()
+        |> Poison.decode!()
+
+      items = get_in(data, ["object", "replies", "first", "items"])
+      assert length(items) > 0
+
+      %{data: data, items: items}
+    end
+
+    test "schedules background fetching of `replies` items if max thread depth limit allows", %{
+      data: data,
+      items: items
+    } do
+      Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10)
+
+      {:ok, _activity} = Transmogrifier.handle_incoming(data)
+
+      for id <- items do
+        job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
+        assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
+      end
+    end
+
+    test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
+         %{data: data} do
+      Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+      {:ok, _activity} = Transmogrifier.handle_incoming(data)
+
+      assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
+    end
+  end
+
+  describe "`handle_incoming/2`, Pleroma format `replies` handling" do
+    clear_config([:activitypub, :note_replies_output_limit]) do
+      Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
+    end
+
+    clear_config([:instance, :federation_incoming_replies_max_depth])
+
+    setup do
+      user = insert(:user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "post1"})
+
+      {:ok, reply1} =
+        CommonAPI.post(user, %{"status" => "reply1", "in_reply_to_status_id" => activity.id})
+
+      {:ok, reply2} =
+        CommonAPI.post(user, %{"status" => "reply2", "in_reply_to_status_id" => activity.id})
+
+      replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end)
+
+      {:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data)
+
+      Repo.delete(activity.object)
+      Repo.delete(activity)
+
+      %{federation_output: federation_output, replies_uris: replies_uris}
+    end
+
+    test "schedules background fetching of `replies` items if max thread depth limit allows", %{
+      federation_output: federation_output,
+      replies_uris: replies_uris
+    } do
+      Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1)
+
+      {:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
+
+      for id <- replies_uris do
+        job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
+        assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
+      end
+    end
+
+    test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
+         %{federation_output: federation_output} do
+      Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+      {:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
+
+      assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
+    end
+  end
+
   describe "prepare outgoing" do
     test "it inlines private announced objects" do
       user = insert(:user)
   describe "prepare outgoing" do
     test "it inlines private announced objects" do
       user = insert(:user)
@@ -1464,7 +1702,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
         "type" => "Announce"
       }
 
         "type" => "Announce"
       }
 
-      :error = Transmogrifier.handle_incoming(data)
+      assert capture_log(fn ->
+               :error = Transmogrifier.handle_incoming(data)
+             end) =~ "Object containment failed"
     end
 
     test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
     end
 
     test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
@@ -1477,7 +1717,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
         "type" => "Announce"
       }
 
         "type" => "Announce"
       }
 
-      :error = Transmogrifier.handle_incoming(data)
+      assert capture_log(fn ->
+               :error = Transmogrifier.handle_incoming(data)
+             end) =~ "Object containment failed"
     end
 
     test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
     end
 
     test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
@@ -1490,7 +1732,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
         "type" => "Announce"
       }
 
         "type" => "Announce"
       }
 
-      :error = Transmogrifier.handle_incoming(data)
+      assert capture_log(fn ->
+               :error = Transmogrifier.handle_incoming(data)
+             end) =~ "Object containment failed"
     end
   end
 
     end
   end
 
@@ -1679,6 +1923,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert modified_object["inReplyToAtomUri"] == ""
     end
 
       assert modified_object["inReplyToAtomUri"] == ""
     end
 
+    @tag capture_log: true
     test "returns modified object when allowed incoming reply", %{data: data} do
       object_with_reply =
         Map.put(
     test "returns modified object when allowed incoming reply", %{data: data} do
       object_with_reply =
         Map.put(
@@ -1793,9 +2038,12 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
 
   describe "get_obj_helper/2" do
     test "returns nil when cannot normalize object" do
 
   describe "get_obj_helper/2" do
     test "returns nil when cannot normalize object" do
-      refute Transmogrifier.get_obj_helper("test-obj-id")
+      assert capture_log(fn ->
+               refute Transmogrifier.get_obj_helper("test-obj-id")
+             end) =~ "Unsupported URI scheme"
     end
 
     end
 
+    @tag capture_log: true
     test "returns {:ok, %Object{}} for success case" do
       assert {:ok, %Object{}} =
                Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
     test "returns {:ok, %Object{}} for success case" do
       assert {:ok, %Object{}} =
                Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
@@ -1895,4 +2143,49 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
              }
     end
   end
              }
     end
   end
+
+  describe "set_replies/1" do
+    clear_config([:activitypub, :note_replies_output_limit]) do
+      Pleroma.Config.put([:activitypub, :note_replies_output_limit], 2)
+    end
+
+    test "returns unmodified object if activity doesn't have self-replies" do
+      data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+      assert Transmogrifier.set_replies(data) == data
+    end
+
+    test "sets `replies` collection with a limited number of self-replies" do
+      [user, another_user] = insert_list(2, :user)
+
+      {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{"status" => "1"})
+
+      {:ok, %{id: id2} = self_reply1} =
+        CommonAPI.post(user, %{"status" => "self-reply 1", "in_reply_to_status_id" => id1})
+
+      {:ok, self_reply2} =
+        CommonAPI.post(user, %{"status" => "self-reply 2", "in_reply_to_status_id" => id1})
+
+      # Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2
+      {:ok, _} =
+        CommonAPI.post(user, %{"status" => "self-reply 3", "in_reply_to_status_id" => id1})
+
+      {:ok, _} =
+        CommonAPI.post(user, %{
+          "status" => "self-reply to self-reply",
+          "in_reply_to_status_id" => id2
+        })
+
+      {:ok, _} =
+        CommonAPI.post(another_user, %{
+          "status" => "another user's reply",
+          "in_reply_to_status_id" => id1
+        })
+
+      object = Object.normalize(activity)
+      replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
+
+      assert %{"type" => "Collection", "items" => ^replies_uris} =
+               Transmogrifier.set_replies(object.data)["replies"]
+    end
+  end
 end
 end