Merge branch 'develop' into fix/csp-mediaproxy-base-url
[akkoma] / test / web / activity_pub / object_validator_test.exs
index 4d90a0cf347abf5232ca1685949a11dd3d896416..31224abe0b99f45a2eaea72780b77a49f8bad957 100644 (file)
@@ -2,18 +2,308 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
   use Pleroma.DataCase
 
   alias Pleroma.Object
   use Pleroma.DataCase
 
   alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.ObjectValidator
   alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.ObjectValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
 
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
 
+  describe "attachments" do
+    test "works with honkerific attachments" do
+      attachment = %{
+        "mediaType" => "",
+        "name" => "",
+        "summary" => "298p3RG7j27tfsZ9RQ.jpg",
+        "type" => "Document",
+        "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
+      }
+
+      assert {:ok, attachment} =
+               AttachmentValidator.cast_and_validate(attachment)
+               |> Ecto.Changeset.apply_action(:insert)
+
+      assert attachment.mediaType == "application/octet-stream"
+    end
+
+    test "it turns mastodon attachments into our attachments" do
+      attachment = %{
+        "url" =>
+          "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
+        "type" => "Document",
+        "name" => nil,
+        "mediaType" => "image/jpeg"
+      }
+
+      {:ok, attachment} =
+        AttachmentValidator.cast_and_validate(attachment)
+        |> Ecto.Changeset.apply_action(:insert)
+
+      assert [
+               %{
+                 href:
+                   "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
+                 type: "Link",
+                 mediaType: "image/jpeg"
+               }
+             ] = attachment.url
+
+      assert attachment.mediaType == "image/jpeg"
+    end
+
+    test "it handles our own uploads" do
+      user = insert(:user)
+
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+      {:ok, attachment} =
+        attachment.data
+        |> AttachmentValidator.cast_and_validate()
+        |> Ecto.Changeset.apply_action(:insert)
+
+      assert attachment.mediaType == "image/jpeg"
+    end
+  end
+
+  describe "chat message create activities" do
+    test "it is invalid if the object already exists" do
+      user = insert(:user)
+      recipient = insert(:user)
+      {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey")
+      object = Object.normalize(activity, false)
+
+      {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id])
+
+      {:error, cng} = ObjectValidator.validate(create_data, [])
+
+      assert {:object, {"The object to create already exists", []}} in cng.errors
+    end
+
+    test "it is invalid if the object data has a different `to` or `actor` field" do
+      user = insert(:user)
+      recipient = insert(:user)
+      {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey")
+
+      {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id])
+
+      {:error, cng} = ObjectValidator.validate(create_data, [])
+
+      assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors
+      assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors
+    end
+  end
+
+  describe "chat messages" do
+    setup do
+      clear_config([:instance, :remote_limit])
+      user = insert(:user)
+      recipient = insert(:user, local: false)
+
+      {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:")
+
+      %{user: user, recipient: recipient, valid_chat_message: valid_chat_message}
+    end
+
+    test "let's through some basic html", %{user: user, recipient: recipient} do
+      {:ok, valid_chat_message, _} =
+        Builder.chat_message(
+          user,
+          recipient.ap_id,
+          "hey <a href='https://example.org'>example</a> <script>alert('uguu')</script>"
+        )
+
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert object["content"] ==
+               "hey <a href=\"https://example.org\">example</a> alert(&#39;uguu&#39;)"
+    end
+
+    test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert Map.put(valid_chat_message, "attachment", nil) == object
+    end
+
+    test "validates for a basic object with an attachment", %{
+      valid_chat_message: valid_chat_message,
+      user: user
+    } do
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+      valid_chat_message =
+        valid_chat_message
+        |> Map.put("attachment", attachment.data)
+
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert object["attachment"]
+    end
+
+    test "validates for a basic object with an attachment in an array", %{
+      valid_chat_message: valid_chat_message,
+      user: user
+    } do
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+      valid_chat_message =
+        valid_chat_message
+        |> Map.put("attachment", [attachment.data])
+
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert object["attachment"]
+    end
+
+    test "validates for a basic object with an attachment but without content", %{
+      valid_chat_message: valid_chat_message,
+      user: user
+    } do
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+      valid_chat_message =
+        valid_chat_message
+        |> Map.put("attachment", attachment.data)
+        |> Map.delete("content")
+
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert object["attachment"]
+    end
+
+    test "does not validate if the message has no content", %{
+      valid_chat_message: valid_chat_message
+    } do
+      contentless =
+        valid_chat_message
+        |> Map.delete("content")
+
+      refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, []))
+    end
+
+    test "does not validate if the message is longer than the remote_limit", %{
+      valid_chat_message: valid_chat_message
+    } do
+      Pleroma.Config.put([:instance, :remote_limit], 2)
+      refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+    end
+
+    test "does not validate if the recipient is blocking the actor", %{
+      valid_chat_message: valid_chat_message,
+      user: user,
+      recipient: recipient
+    } do
+      Pleroma.User.block(recipient, user)
+      refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+    end
+
+    test "does not validate if the actor or the recipient is not in our system", %{
+      valid_chat_message: valid_chat_message
+    } do
+      chat_message =
+        valid_chat_message
+        |> Map.put("actor", "https://raymoo.com/raymoo")
+
+      {:error, _} = ObjectValidator.validate(chat_message, [])
+
+      chat_message =
+        valid_chat_message
+        |> Map.put("to", ["https://raymoo.com/raymoo"])
+
+      {:error, _} = ObjectValidator.validate(chat_message, [])
+    end
+
+    test "does not validate for a message with multiple recipients", %{
+      valid_chat_message: valid_chat_message,
+      user: user,
+      recipient: recipient
+    } do
+      chat_message =
+        valid_chat_message
+        |> Map.put("to", [user.ap_id, recipient.ap_id])
+
+      assert {:error, _} = ObjectValidator.validate(chat_message, [])
+    end
+
+    test "does not validate if it doesn't concern local users" do
+      user = insert(:user, local: false)
+      recipient = insert(:user, local: false)
+
+      {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey")
+      assert {:error, _} = ObjectValidator.validate(valid_chat_message, [])
+    end
+  end
+
+  describe "EmojiReacts" do
+    setup do
+      user = insert(:user)
+      {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
+
+      object = Pleroma.Object.get_by_ap_id(post_activity.data["object"])
+
+      {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌")
+
+      %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react}
+    end
+
+    test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do
+      assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, [])
+    end
+
+    test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do
+      without_content =
+        valid_emoji_react
+        |> Map.delete("content")
+
+      {:error, cng} = ObjectValidator.validate(without_content, [])
+
+      refute cng.valid?
+      assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
+    end
+
+    test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
+      without_emoji_content =
+        valid_emoji_react
+        |> Map.put("content", "x")
+
+      {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
+
+      refute cng.valid?
+
+      assert {:content, {"must be a single character emoji", []}} in cng.errors
+    end
+  end
+
   describe "Undos" do
     setup do
       user = insert(:user)
   describe "Undos" do
     setup do
       user = insert(:user)
-      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
+      {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
       {:ok, like} = CommonAPI.favorite(user, post_activity.id)
       {:ok, valid_like_undo, []} = Builder.undo(user, like)
 
       {:ok, like} = CommonAPI.favorite(user, post_activity.id)
       {:ok, valid_like_undo, []} = Builder.undo(user, like)
 
@@ -53,7 +343,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
   describe "deletes" do
     setup do
       user = insert(:user)
   describe "deletes" do
     setup do
       user = insert(:user)
-      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "cancel me daddy"})
+      {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"})
 
       {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"])
       {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id)
 
       {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"])
       {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id)
@@ -107,7 +397,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
       {:error, cng} = ObjectValidator.validate(missing_object, [])
 
       assert {:object, {"can't find object", []}} in cng.errors
       {:error, cng} = ObjectValidator.validate(missing_object, [])
 
       assert {:object, {"can't find object", []}} in cng.errors
-      assert length(cng.errors) == 1
     end
 
     test "it's invalid if the actor of the object and the actor of delete are from different domains",
     end
 
     test "it's invalid if the actor of the object and the actor of delete are from different domains",
@@ -146,7 +435,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
   describe "likes" do
     setup do
       user = insert(:user)
   describe "likes" do
     setup do
       user = insert(:user)
-      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
+      {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
 
       valid_like = %{
         "to" => [user.ap_id],
 
       valid_like = %{
         "to" => [user.ap_id],
@@ -241,4 +530,96 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
       assert {:object, valid_like["object"]} in validated.changes
     end
   end
       assert {:object, valid_like["object"]} in validated.changes
     end
   end
+
+  describe "announces" do
+    setup do
+      user = insert(:user)
+      announcer = insert(:user)
+      {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
+
+      object = Object.normalize(post_activity, false)
+      {:ok, valid_announce, []} = Builder.announce(announcer, object)
+
+      %{
+        valid_announce: valid_announce,
+        user: user,
+        post_activity: post_activity,
+        announcer: announcer
+      }
+    end
+
+    test "returns ok for a valid announce", %{valid_announce: valid_announce} do
+      assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, [])
+    end
+
+    test "returns an error if the object can't be found", %{valid_announce: valid_announce} do
+      without_object =
+        valid_announce
+        |> Map.delete("object")
+
+      {:error, cng} = ObjectValidator.validate(without_object, [])
+
+      assert {:object, {"can't be blank", [validation: :required]}} in cng.errors
+
+      nonexisting_object =
+        valid_announce
+        |> Map.put("object", "https://gensokyo.2hu/objects/99999999")
+
+      {:error, cng} = ObjectValidator.validate(nonexisting_object, [])
+
+      assert {:object, {"can't find object", []}} in cng.errors
+    end
+
+    test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do
+      nonexisting_actor =
+        valid_announce
+        |> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
+
+      {:error, cng} = ObjectValidator.validate(nonexisting_actor, [])
+
+      assert {:actor, {"can't find user", []}} in cng.errors
+    end
+
+    test "returns an error if the actor already announced the object", %{
+      valid_announce: valid_announce,
+      announcer: announcer,
+      post_activity: post_activity
+    } do
+      _announce = CommonAPI.repeat(post_activity.id, announcer)
+
+      {:error, cng} = ObjectValidator.validate(valid_announce, [])
+
+      assert {:actor, {"already announced this object", []}} in cng.errors
+      assert {:object, {"already announced by this actor", []}} in cng.errors
+    end
+
+    test "returns an error if the actor can't announce the object", %{
+      announcer: announcer,
+      user: user
+    } do
+      {:ok, post_activity} =
+        CommonAPI.post(user, %{status: "a secret post", visibility: "private"})
+
+      object = Object.normalize(post_activity, false)
+
+      # Another user can't announce it
+      {:ok, announce, []} = Builder.announce(announcer, object, public: false)
+
+      {:error, cng} = ObjectValidator.validate(announce, [])
+
+      assert {:actor, {"can not announce this object", []}} in cng.errors
+
+      # The actor of the object can announce it
+      {:ok, announce, []} = Builder.announce(user, object, public: false)
+
+      assert {:ok, _, _} = ObjectValidator.validate(announce, [])
+
+      # The actor of the object can not announce it publicly
+      {:ok, announce, []} = Builder.announce(user, object, public: true)
+
+      {:error, cng} = ObjectValidator.validate(announce, [])
+
+      assert {:actor, {"can not announce this object publicly", []}} in cng.errors
+    end
+  end
 end
 end