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