From bc99e48f0663a8d3ffb3add2913df43ae4f1e6f8 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Thu, 4 Mar 2021 11:43:35 +0100
Subject: [PATCH] Add tests for discussion

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 lib/graphql/error.ex                       |   1 +
 lib/graphql/resolvers/discussion.ex        |  17 +-
 lib/mobilizon/discussions/discussions.ex   |  10 +-
 test/graphql/resolvers/discussion_test.exs | 558 +++++++++++++++++++++
 test/support/factory.ex                    |   5 +-
 5 files changed, 582 insertions(+), 9 deletions(-)
 create mode 100644 test/graphql/resolvers/discussion_test.exs

diff --git a/lib/graphql/error.ex b/lib/graphql/error.ex
index 6d5def169..653a09051 100644
--- a/lib/graphql/error.ex
+++ b/lib/graphql/error.ex
@@ -89,6 +89,7 @@ defmodule Mobilizon.GraphQL.Error do
   defp metadata(:event_not_found), do: {404, dgettext("errors", "Event not found")}
   defp metadata(:group_not_found), do: {404, dgettext("errors", "Group not found")}
   defp metadata(:resource_not_found), do: {404, dgettext("errors", "Resource not found")}
+  defp metadata(:discussion_not_found), do: {404, dgettext("errors", "Discussion not found")}
   defp metadata(:unknown), do: {500, dgettext("errors", "Something went wrong")}
 
   defp metadata(code) do
diff --git a/lib/graphql/resolvers/discussion.ex b/lib/graphql/resolvers/discussion.ex
index bf49da5ac..cfe4f5b7b 100644
--- a/lib/graphql/resolvers/discussion.ex
+++ b/lib/graphql/resolvers/discussion.ex
@@ -109,6 +109,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
     end
   end
 
+  def create_discussion(_, _, _), do: {:error, :unauthenticated}
+
   def reply_to_discussion(
         _parent,
         %{text: text, discussion_id: discussion_id},
@@ -141,9 +143,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
                origin_comment_id || previous_in_reply_to_comment_id || last_comment_id
            }) do
       {:ok, discussion}
+    else
+      {:no_discussion, _} ->
+        {:error, :discussion_not_found}
     end
   end
 
+  def reply_to_discussion(_, _, _), do: {:error, :unauthenticated}
+
   @spec update_discussion(map(), map(), map()) :: {:ok, Discussion.t()}
   def update_discussion(
         _parent,
@@ -166,9 +173,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
              }
            ) do
       {:ok, discussion}
+    else
+      {:member, false} ->
+        {:error, :unauthorized}
     end
   end
 
+  def update_discussion(_, _, _), do: {:error, :unauthenticated}
+
   def delete_discussion(_parent, %{discussion_id: discussion_id}, %{
         context: %{
           current_user: %User{} = user
@@ -186,8 +198,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
         {:error, dgettext("errors", "No discussion with ID %{id}", id: discussion_id)}
 
       {:member, _} ->
-        {:error,
-         dgettext("errors", "You are not a member of the group the discussion belongs to")}
+        {:error, :unauthorized}
     end
   end
+
+  def delete_discussion(_, _, _), do: {:error, :unauthenticated}
 end
diff --git a/lib/mobilizon/discussions/discussions.ex b/lib/mobilizon/discussions/discussions.ex
index fac154fc9..e951d4604 100644
--- a/lib/mobilizon/discussions/discussions.ex
+++ b/lib/mobilizon/discussions/discussions.ex
@@ -329,7 +329,7 @@ defmodule Mobilizon.Discussions do
   @doc """
   Get a discussion by it's ID
   """
-  @spec get_discussion(String.t() | integer()) :: Discussion.t()
+  @spec get_discussion(String.t() | integer()) :: Discussion.t() | nil
   def get_discussion(discussion_id) do
     Discussion
     |> Repo.get(discussion_id)
@@ -399,7 +399,8 @@ defmodule Mobilizon.Discussions do
                                                    } ->
              Changeset.change(comment, %{discussion_id: discussion_id, url: discussion_url})
            end)
-           |> Repo.transaction() do
+           |> Repo.transaction(),
+         %Discussion{} = discussion <- Repo.preload(discussion, @discussion_preloads) do
       {:ok, discussion}
     end
   end
@@ -427,8 +428,9 @@ defmodule Mobilizon.Discussions do
                %{last_comment_id: comment_id}
              )
            end)
-           |> Repo.transaction() do
-      # Discussion is not updated
+           |> Repo.transaction(),
+         # Discussion is not updated
+         %Comment{} = comment <- Repo.preload(comment, @comment_preloads) do
       {:ok, Map.put(discussion, :last_comment, comment)}
     end
   end
diff --git a/test/graphql/resolvers/discussion_test.exs b/test/graphql/resolvers/discussion_test.exs
new file mode 100644
index 000000000..e0adc56ec
--- /dev/null
+++ b/test/graphql/resolvers/discussion_test.exs
@@ -0,0 +1,558 @@
+defmodule Mobilizon.GraphQL.Resolvers.DiscussionTest do
+  use Mobilizon.Web.ConnCase
+
+  import Mobilizon.Factory
+
+  alias Mobilizon.Actors.Actor
+  alias Mobilizon.Discussions
+  alias Mobilizon.Discussions.{Comment, Discussion}
+  alias Mobilizon.GraphQL.AbsintheHelpers
+
+  @comment_text "What do you think?"
+  @discussion_title "Hey, I'm a title!"
+
+  setup %{conn: conn} do
+    user = insert(:user)
+    actor = insert(:actor, user: user)
+    group = insert(:group)
+    insert(:member, role: :member, parent: group, actor: actor)
+
+    {:ok, conn: conn, actor: actor, user: user, group: group}
+  end
+
+  @discussion_fields_fragment """
+  fragment DiscussionFields on Discussion {
+    id
+    title
+    slug
+    lastComment {
+      id
+      text
+      insertedAt
+      updatedAt
+      actor {
+        id
+      }
+    }
+    actor {
+      id
+      domain
+      name
+      preferredUsername
+    }
+    creator {
+      id
+      domain
+      name
+      preferredUsername
+    }
+  }
+  """
+
+  describe "create a discussion" do
+    @create_discussion_mutation """
+    mutation createDiscussion($title: String!, $actorId: ID!, $text: String!) {
+    createDiscussion(title: $title, text: $text, actorId: $actorId) {
+      ...DiscussionFields
+    }
+    }
+    #{@discussion_fields_fragment}
+    """
+
+    test "create_discussion/3 creates a discussion", %{
+      conn: conn,
+      actor: actor,
+      user: user,
+      group: group
+    } do
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @create_discussion_mutation,
+          variables: %{text: @comment_text, actorId: group.id, title: @discussion_title}
+        )
+
+      assert res["errors"] == nil
+      assert res["data"]["createDiscussion"]["actor"]["id"] == to_string(group.id)
+      assert res["data"]["createDiscussion"]["creator"]["id"] == to_string(actor.id)
+
+      assert res["data"]["createDiscussion"]["title"] == @discussion_title
+      assert res["data"]["createDiscussion"]["lastComment"]["text"] == @comment_text
+
+      assert res["data"]["createDiscussion"]["lastComment"]["actor"]["id"] ==
+               to_string(actor.id)
+    end
+
+    test "create_discussion/3 doesn't work if the actor is not a member", %{
+      conn: conn,
+      group: group
+    } do
+      user = insert(:user)
+      insert(:actor, user: user)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @create_discussion_mutation,
+          variables: %{text: @comment_text, actorId: group.id, title: @discussion_title}
+        )
+
+      assert hd(res["errors"])["code"] == "unauthorized"
+    end
+
+    test "create_discussion/3 doesn't work if the actor is not an approved member", %{
+      conn: conn,
+      group: group
+    } do
+      user = insert(:user)
+      actor = insert(:actor, user: user)
+      insert(:member, role: :invited, actor: actor, parent: group)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @create_discussion_mutation,
+          variables: %{text: @comment_text, actorId: group.id, title: @discussion_title}
+        )
+
+      assert hd(res["errors"])["code"] == "unauthorized"
+    end
+
+    test "create_discussion/3 doesn't work if the user isn't logged-in", %{
+      conn: conn,
+      group: group
+    } do
+      res =
+        conn
+        |> AbsintheHelpers.graphql_query(
+          query: @create_discussion_mutation,
+          variables: %{text: @comment_text, actorId: group.id, title: @discussion_title}
+        )
+
+      assert hd(res["errors"])["code"] == "unauthenticated"
+    end
+  end
+
+  describe "reply to a discussion" do
+    @reply_to_discussion_mutation """
+    mutation replyToDiscussion($discussionId: ID!, $text: String!) {
+      replyToDiscussion(discussionId: $discussionId, text: $text) {
+        ...DiscussionFields
+      }
+    }
+    #{@discussion_fields_fragment}
+    """
+
+    @reply_text "I agree with that."
+
+    test "reply_to_discussion/3 replies to a discussion", %{
+      conn: conn,
+      actor: actor,
+      group: group
+    } do
+      %Discussion{id: discussion_id} = insert_discussion(group, actor)
+
+      user = insert(:user)
+      actor2 = insert(:actor, user: user)
+      insert(:member, role: :member, parent: group, actor: actor2)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @reply_to_discussion_mutation,
+          variables: %{text: @reply_text, discussionId: discussion_id}
+        )
+
+      assert res["errors"] == nil
+      assert res["data"]["replyToDiscussion"]["actor"]["id"] == to_string(group.id)
+      assert res["data"]["replyToDiscussion"]["creator"]["id"] == to_string(actor.id)
+
+      assert res["data"]["replyToDiscussion"]["lastComment"]["actor"]["id"] ==
+               to_string(actor2.id)
+
+      assert res["data"]["replyToDiscussion"]["lastComment"]["text"] == @reply_text
+    end
+  end
+
+  describe "Update a discussion" do
+    @update_a_discussion_mutation """
+    mutation updateDiscussion($discussionId: ID!, $title: String!) {
+      updateDiscussion(discussionId: $discussionId, title: $title) {
+        ...DiscussionFields
+      }
+    }
+    #{@discussion_fields_fragment}
+    """
+
+    @updated_title "New title for discussion"
+
+    test "update_a_discussion/3 updates a discussion as original creator", %{
+      conn: conn,
+      user: user,
+      actor: actor,
+      group: group
+    } do
+      %Discussion{id: discussion_id} = insert_discussion(group, actor)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @update_a_discussion_mutation,
+          variables: %{title: @updated_title, discussionId: discussion_id}
+        )
+
+      assert res["errors"] == nil
+      assert res["data"]["updateDiscussion"]["title"] == @updated_title
+    end
+
+    test "update_a_discussion/3 doesn't update a discussion if not member", %{
+      conn: conn,
+      actor: actor,
+      group: group
+    } do
+      %Discussion{id: discussion_id} = insert_discussion(group, actor)
+
+      user = insert(:user)
+      actor2 = insert(:actor, user: user)
+      insert(:member, role: :invited, parent: group, actor: actor2)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @update_a_discussion_mutation,
+          variables: %{title: @updated_title, discussionId: discussion_id}
+        )
+
+      assert hd(res["errors"])["code"] == "unauthorized"
+    end
+
+    test "update_a_discussion/3 doesn't update a discussion if not logged in", %{
+      conn: conn,
+      actor: actor,
+      group: group
+    } do
+      %Discussion{id: discussion_id} = insert_discussion(group, actor)
+
+      res =
+        conn
+        |> AbsintheHelpers.graphql_query(
+          query: @update_a_discussion_mutation,
+          variables: %{title: @updated_title, discussionId: discussion_id}
+        )
+
+      assert hd(res["errors"])["code"] == "unauthenticated"
+    end
+  end
+
+  describe "Delete a discussion" do
+    @delete_discussion_mutation """
+    mutation deleteDiscussion($discussionId: ID!) {
+      deleteDiscussion(discussionId: $discussionId) {
+        id
+      }
+    }
+    """
+
+    test "delete_discussion/3 deletes a discussion", %{
+      conn: conn,
+      user: user,
+      actor: actor,
+      group: group
+    } do
+      %Discussion{id: discussion_id} = insert_discussion(group, actor)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @delete_discussion_mutation,
+          variables: %{discussionId: discussion_id}
+        )
+
+      assert res["errors"] == nil
+      assert res["data"]["deleteDiscussion"]["id"] == to_string(discussion_id)
+
+      assert nil == Discussions.get_discussion(discussion_id)
+    end
+
+    test "delete_discussion/3 doesn't delete a discussion if not member", %{
+      conn: conn,
+      actor: actor,
+      group: group
+    } do
+      %Discussion{id: discussion_id} = insert_discussion(group, actor)
+
+      user = insert(:user)
+      actor2 = insert(:actor, user: user)
+      insert(:member, role: :invited, parent: group, actor: actor2)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @delete_discussion_mutation,
+          variables: %{discussionId: discussion_id}
+        )
+
+      assert hd(res["errors"])["code"] == "unauthorized"
+      refute nil == Discussions.get_discussion(discussion_id)
+    end
+
+    test "delete_discussion/3 doesn't delete a discussion if not logged in", %{
+      conn: conn,
+      actor: actor,
+      group: group
+    } do
+      %Discussion{id: discussion_id} = insert_discussion(group, actor)
+
+      res =
+        conn
+        |> AbsintheHelpers.graphql_query(
+          query: @delete_discussion_mutation,
+          variables: %{discussionId: discussion_id}
+        )
+
+      assert hd(res["errors"])["code"] == "unauthenticated"
+      refute nil == Discussions.get_discussion(discussion_id)
+    end
+
+    # test "create_comment/3 doesn't allow creating events if it's disabled", %{
+    #   conn: conn,
+    #   actor: actor,
+    #   user: user,
+    #   event: event
+    # } do
+    #   {:ok, %Event{options: %EventOptions{comment_moderation: :closed}}} =
+    #     Events.update_event(event, %{options: %{comment_moderation: :closed}})
+
+    #   res =
+    #     conn
+    #     |> auth_conn(user)
+    #     |> AbsintheHelpers.graphql_query(
+    #       query: @create_comment_mutation,
+    #       variables: %{text: @comment_text, eventId: event.id}
+    #     )
+
+    #   assert hd(res["errors"])["message"] ==
+    #            "You don't have permission to do this"
+    # end
+
+    # test "create_comment/3 allows creating events if it's disabled but we're the organizer", %{
+    #   conn: conn,
+    #   actor: actor,
+    #   user: user
+    # } do
+    #   event = insert(:event, organizer_actor: actor, options: %{comment_moderation: :closed})
+
+    #   res =
+    #     conn
+    #     |> auth_conn(user)
+    #     |> AbsintheHelpers.graphql_query(
+    #       query: @create_comment_mutation,
+    #       variables: %{text: @comment_text, eventId: event.id}
+    #     )
+
+    #   assert is_nil(res["errors"])
+    #   assert res["data"]["createComment"]["text"] == @comment_text
+    # end
+
+    # test "create_comment/3 requires that the user needs to be authenticated", %{
+    #   conn: conn,
+    #   event: event
+    # } do
+    #   actor = insert(:actor)
+
+    #   res =
+    #     conn
+    #     |> AbsintheHelpers.graphql_query(
+    #       query: @create_comment_mutation,
+    #       variables: %{text: @comment_text, eventId: event.id}
+    #     )
+
+    #   assert hd(res["errors"])["message"] ==
+    #            "You are not allowed to create a comment if not connected"
+    # end
+
+    # test "create_comment/3 creates a reply to a comment", %{
+    #   conn: conn,
+    #   actor: actor,
+    #   user: user,
+    #   event: event
+    # } do
+    #   comment = insert(:comment)
+
+    #   res =
+    #     conn
+    #     |> auth_conn(user)
+    #     |> AbsintheHelpers.graphql_query(
+    #       query: @create_comment_mutation,
+    #       variables: %{
+    #         text: @comment_text,
+    #         eventId: event.id,
+    #         inReplyToCommentId: comment.id
+    #       }
+    #     )
+
+    #   assert is_nil(res["errors"])
+    #   assert res["data"]["createComment"]["text"] == @comment_text
+    #   uuid = res["data"]["createComment"]["uuid"]
+
+    #   assert res["data"]["createComment"]["inReplyToComment"]["id"] ==
+    #            to_string(comment.id)
+
+    #   query = """
+    #   query {
+    #     thread(id: #{comment.id}) {
+    #       text,
+    #       uuid
+    #     }
+    #   }
+    #   """
+
+    #   res =
+    #     conn
+    #     |> auth_conn(user)
+    #     |> AbsintheHelpers.graphql_query(query: query, variables: %{})
+
+    #   assert res["errors"] == nil
+    #   assert res["data"]["thread"] == [%{"uuid" => uuid, "text" => @comment_text}]
+    # end
+
+    # @delete_comment """
+    #   mutation DeleteComment($commentId: ID!) {
+    #     deleteComment(commentId: $commentId) {
+    #       id,
+    #       deletedAt
+    #     }
+    #   }
+    # """
+
+    # test "deletes a comment", %{conn: conn, user: user, actor: actor} do
+    #   comment = insert(:comment, actor: actor)
+
+    #   res =
+    #     conn
+    #     |> AbsintheHelpers.graphql_query(
+    #       query: @delete_comment,
+    #       variables: %{commentId: comment.id}
+    #     )
+
+    #   assert hd(res["errors"])["message"] ==
+    #            "You are not allowed to delete a comment if not connected"
+
+    #   # Change the current actor for user
+    #   actor2 = insert(:actor, user: user)
+    #   Mobilizon.Users.update_user_default_actor(user.id, actor2.id)
+
+    #   res =
+    #     conn
+    #     |> auth_conn(user)
+    #     |> AbsintheHelpers.graphql_query(
+    #       query: @delete_comment,
+    #       variables: %{commentId: comment.id}
+    #     )
+
+    #   assert hd(res["errors"])["message"] ==
+    #            "You cannot delete this comment"
+
+    #   Mobilizon.Users.update_user_default_actor(user.id, actor.id)
+
+    #   res =
+    #     conn
+    #     |> auth_conn(user)
+    #     |> AbsintheHelpers.graphql_query(
+    #       query: @delete_comment,
+    #       variables: %{commentId: comment.id}
+    #     )
+
+    #   assert res["errors"] == nil
+    #   assert res["data"]["deleteComment"]["id"] == to_string(comment.id)
+    #   refute is_nil(res["data"]["deleteComment"]["deletedAt"])
+    # end
+
+    # test "delete_comment/3 allows a comment being deleted by a moderator and creates a entry in actionLogs",
+    #      %{
+    #        conn: conn,
+    #        user: _user,
+    #        actor: _actor
+    #      } do
+    #   user_moderator = insert(:user, role: :moderator)
+    #   actor_moderator = insert(:actor, user: user_moderator)
+
+    #   actor2 = insert(:actor)
+    #   comment = insert(:comment, actor: actor2)
+
+    #   res =
+    #     conn
+    #     |> auth_conn(user_moderator)
+    #     |> AbsintheHelpers.graphql_query(
+    #       query: @delete_comment,
+    #       variables: %{commentId: comment.id}
+    #     )
+
+    #   assert res["data"]["deleteComment"]["id"] == to_string(comment.id)
+
+    #   query = """
+    #   {
+    #     actionLogs {
+    #       action,
+    #       actor {
+    #         preferredUsername
+    #       },
+    #       object {
+    #         ... on Report {
+    #           id,
+    #           status
+    #         },
+    #         ... on ReportNote {
+    #           content
+    #         }
+    #         ... on Event {
+    #           id,
+    #           title
+    #         },
+    #         ... on Comment {
+    #           id,
+    #           text
+    #         }
+    #       }
+    #     }
+    #   }
+    #   """
+
+    #   res =
+    #     conn
+    #     |> auth_conn(user_moderator)
+    #     |> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
+
+    #   refute json_response(res, 200)["errors"]
+
+    #   assert hd(json_response(res, 200)["data"]["actionLogs"]) == %{
+    #            "action" => "COMMENT_DELETION",
+    #            "actor" => %{"preferredUsername" => actor_moderator.preferred_username},
+    #            "object" => %{"text" => comment.text, "id" => to_string(comment.id)}
+    #          }
+    # end
+  end
+
+  @spec insert_discussion(Actor.t(), Actor.t()) :: Discussion.t()
+  defp insert_discussion(%Actor{type: :Group} = group, %Actor{} = actor) do
+    %Comment{id: comment_id} = comment = insert(:comment)
+
+    %Discussion{id: discussion_id} =
+      discussion = insert(:discussion, creator: actor, actor: group)
+
+    Discussions.update_comment(comment, %{discussion_id: discussion_id})
+
+    {:ok, %Discussion{} = discussion} =
+      Discussions.update_discussion(discussion, %{last_comment_id: comment_id})
+
+    discussion
+  end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 0f4d54ada..e956f255d 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -395,7 +395,6 @@ defmodule Mobilizon.Factory do
     uuid = Ecto.UUID.generate()
     actor = build(:actor)
     group = build(:group)
-    comment = build(:comment, actor: actor, attributed_to: group)
     slug = "my-awesome-discussion-#{ShortUUID.encode!(uuid)}"
 
     %Mobilizon.Discussions.Discussion{
@@ -404,8 +403,8 @@ defmodule Mobilizon.Factory do
       creator: actor,
       actor: group,
       id: uuid,
-      last_comment: comment,
-      comments: [comment],
+      last_comment: nil,
+      comments: [],
       url: Routes.page_url(Endpoint, :discussion, group.preferred_username, slug)
     }
   end