From 8a108ce93246869ab2d1cd9a33d51f68ccf27de1 Mon Sep 17 00:00:00 2001 From: Josh Smith Date: Thu, 14 Dec 2017 18:51:00 -0800 Subject: [PATCH] Add conversation part controller --- lib/code_corps/messages/conversation_parts.ex | 28 +++ lib/code_corps/messages/messages.ex | 28 ++- lib/code_corps/policy/conversation_part.ex | 56 ++++++ lib/code_corps/policy/helpers.ex | 9 + lib/code_corps/policy/policy.ex | 43 +++- .../conversation_part_controller.ex | 48 +++++ lib/code_corps_web/router.ex | 1 + .../views/conversation_part_view.ex | 10 + priv/repo/structure.sql | 1 + .../messages/conversation_parts_test.exs | 67 +++++++ .../lib/code_corps/messages/messages_test.exs | 37 +++- .../policy/conversation_part_test.exs | 183 ++++++++++++++++++ .../helpers_test.exs} | 22 +++ .../conversation_part_controller_test.exs | 103 ++++++++++ .../views/conversation_part_view_test.exs | 42 ++++ 15 files changed, 673 insertions(+), 5 deletions(-) create mode 100644 lib/code_corps/messages/conversation_parts.ex create mode 100644 lib/code_corps/policy/conversation_part.ex create mode 100644 lib/code_corps_web/controllers/conversation_part_controller.ex create mode 100644 lib/code_corps_web/views/conversation_part_view.ex create mode 100644 test/lib/code_corps/messages/conversation_parts_test.exs create mode 100644 test/lib/code_corps/policy/conversation_part_test.exs rename test/lib/code_corps/{helpers/policy_test.exs => policy/helpers_test.exs} (90%) create mode 100644 test/lib/code_corps_web/controllers/conversation_part_controller_test.exs create mode 100644 test/lib/code_corps_web/views/conversation_part_view_test.exs diff --git a/lib/code_corps/messages/conversation_parts.ex b/lib/code_corps/messages/conversation_parts.ex new file mode 100644 index 000000000..f33c3cdaa --- /dev/null +++ b/lib/code_corps/messages/conversation_parts.ex @@ -0,0 +1,28 @@ +defmodule CodeCorps.Messages.ConversationParts do + @moduledoc ~S""" + An individual part of a conversation in a `CodeCorps.Conversation` thread, + i.e. a reply to the `CodeCorps.Conversation` by any participant. + """ + + import Ecto.Changeset, only: [assoc_constraint: 2, cast: 3, validate_required: 2] + + alias CodeCorps.{ + ConversationPart, + Repo + } + + @spec create(map) :: ConversationPart.t | Ecto.Changeset.t + def create(attrs) do + %ConversationPart{} |> create_changeset(attrs) |> Repo.insert() + end + + @doc false + @spec create_changeset(ConversationPart.t, map) :: Ecto.Changeset.t + def create_changeset(%ConversationPart{} = conversation_part, attrs) do + conversation_part + |> cast(attrs, [:author_id, :body, :conversation_id, :read_at]) + |> validate_required([:author_id, :body, :conversation_id]) + |> assoc_constraint(:author) + |> assoc_constraint(:conversation) + end +end diff --git a/lib/code_corps/messages/messages.ex b/lib/code_corps/messages/messages.ex index 4fe44e958..4e6847167 100644 --- a/lib/code_corps/messages/messages.ex +++ b/lib/code_corps/messages/messages.ex @@ -3,7 +3,14 @@ defmodule CodeCorps.Messages do Main context for work with the Messaging feature. """ - alias CodeCorps.{Conversation, Helpers.Query, Message, Messages, Repo} + alias CodeCorps.{ + Conversation, + ConversationPart, + Helpers.Query, + Message, + Messages, + Repo + } alias Ecto.{Changeset, Queryable} @doc ~S""" @@ -29,6 +36,14 @@ defmodule CodeCorps.Messages do |> Repo.all() end + @doc ~S""" + Lists pre-scoped `CodeCorps.ConversationPart` records filtered by parameters + """ + @spec list_parts(Queryable.t, map) :: list(Conversation.t) + def list_parts(scope, %{} = _params) do + scope |> Repo.all() + end + @doc ~S""" Gets a `CodeCorps.Conversation` record """ @@ -37,6 +52,14 @@ defmodule CodeCorps.Messages do Conversation |> Repo.get(id) end + @doc ~S""" + Gets a `CodeCorps.ConversationPart` record + """ + @spec get_part(integer) :: Conversation.t + def get_part(id) do + ConversationPart |> Repo.get(id) + end + @doc ~S""" Creates a `CodeCorps.Message` from a set of parameters. """ @@ -46,4 +69,7 @@ defmodule CodeCorps.Messages do |> Message.changeset(params) |> Repo.insert() end + + @spec add_part(map) :: {:ok, ConversationPart.t} | {:error, Changeset.t} + def add_part(map), do: Messages.ConversationParts.create(map) end diff --git a/lib/code_corps/policy/conversation_part.ex b/lib/code_corps/policy/conversation_part.ex new file mode 100644 index 000000000..7d7754165 --- /dev/null +++ b/lib/code_corps/policy/conversation_part.ex @@ -0,0 +1,56 @@ +defmodule CodeCorps.Policy.ConversationPart do + @moduledoc ~S""" + Handles `CodeCorps.User` authorization of actions on `CodeCorps.Conversation` + records. + """ + + import CodeCorps.Policy.Helpers, + only: [ + administered_by?: 2, get_conversation: 1, get_message: 1, get_project: 1 + ] + import Ecto.Query + + alias CodeCorps.{Conversation, ConversationPart, Policy, Repo, User} + + @spec scope(Ecto.Queryable.t, User.t) :: Ecto.Queryable.t + def scope(queryable, %User{admin: true}), do: queryable + def scope(queryable, %User{id: id} = current_user) do + scoped_conversation_ids = + Conversation + |> Policy.Conversation.scope(current_user) + |> select([c], c.id) + |> Repo.all() + + queryable + |> where(author_id: ^id) + |> or_where([cp], cp.conversation_id in ^scoped_conversation_ids) + end + + def create?(%User{} = user, %{"conversation_id" => _} = params) do + authorize(user, params) + end + def create?(_, _), do: false + + def show?(%User{} = user, %ConversationPart{conversation_id: _} = part) do + authorize(user, part) + end + def show?(_, _), do: false + + @spec authorize(User.t, ConversationPart.t | map) :: boolean + defp authorize(%User{} = user, attrs) do + %Conversation{} = conversation = attrs |> get_conversation() + is_target? = conversation |> conversation_target?(user) + + is_admin? = + conversation + |> get_message() + |> get_project() + |> administered_by?(user) + + is_target? or is_admin? + end + + defp conversation_target?(%Conversation{user_id: target_id}, %User{id: user_id}) do + target_id == user_id + end +end diff --git a/lib/code_corps/policy/helpers.ex b/lib/code_corps/policy/helpers.ex index 54a441f2e..b6603d6a9 100644 --- a/lib/code_corps/policy/helpers.ex +++ b/lib/code_corps/policy/helpers.ex @@ -6,6 +6,7 @@ defmodule CodeCorps.Policy.Helpers do alias CodeCorps.{ Conversation, + ConversationPart, Message, Organization, ProjectUser, @@ -100,6 +101,14 @@ defmodule CodeCorps.Policy.Helpers do defp owner?("owner"), do: true defp owner?(_), do: false + @doc """ + Retrieves conversation from associated record + """ + @spec get_conversation(Changeset.t() | ConversationPart.t() | map) :: Message.t() + def get_conversation(%ConversationPart{conversation_id: conversation_id}), do: Repo.get(Conversation, conversation_id) + def get_conversation(%{"conversation_id" => conversation_id}), do: Repo.get(Conversation, conversation_id) + def get_conversation(%Changeset{changes: %{conversation_id: conversation_id}}), do: Repo.get(Conversation, conversation_id) + @doc """ Retrieves message from associated record """ diff --git a/lib/code_corps/policy/policy.ex b/lib/code_corps/policy/policy.ex index f4c615c6a..81be8c44d 100644 --- a/lib/code_corps/policy/policy.ex +++ b/lib/code_corps/policy/policy.ex @@ -3,9 +3,41 @@ defmodule CodeCorps.Policy do Handles authorization for various API actions performed on objects in the database. """ - alias CodeCorps.{Category, Comment, Conversation, DonationGoal, GithubAppInstallation, GithubEvent, GithubRepo, Message, Organization, OrganizationInvite, OrganizationGithubAppInstallation, Preview, Project, ProjectCategory, ProjectSkill, ProjectUser, Role, RoleSkill, Skill, StripeConnectAccount, StripeConnectPlan, StripeConnectSubscription, StripePlatformCard, StripePlatformCustomer, Task, TaskSkill, User, UserCategory, UserRole, UserSkill, UserTask} - - alias CodeCorps.Policy + alias CodeCorps.{ + Category, + Comment, + Conversation, + ConversationPart, + DonationGoal, + GithubAppInstallation, + GithubEvent, + GithubRepo, + Message, + Organization, + OrganizationInvite, + OrganizationGithubAppInstallation, + Policy, + Preview, + Project, + ProjectCategory, + ProjectSkill, + ProjectUser, + Role, + RoleSkill, + Skill, + StripeConnectAccount, + StripeConnectPlan, + StripeConnectSubscription, + StripePlatformCard, + StripePlatformCustomer, + Task, + TaskSkill, + User, + UserCategory, + UserRole, + UserSkill, + UserTask + } @doc ~S""" Determines if the specified user can perform the specified action on the @@ -29,6 +61,7 @@ defmodule CodeCorps.Policy do @spec scope(module, User.t) :: Ecto.Queryable.t def scope(Message, %User{} = current_user), do: Message |> Policy.Message.scope(current_user) def scope(Conversation, %User{} = current_user), do: Conversation |> Policy.Conversation.scope(current_user) + def scope(ConversationPart, %User{} = current_user), do: ConversationPart |> Policy.ConversationPart.scope(current_user) @spec can?(User.t, atom, struct, map) :: boolean @@ -43,6 +76,10 @@ defmodule CodeCorps.Policy do # Conversation defp can?(%User{} = current_user, :show, %Conversation{} = conversation, %{}), do: Policy.Conversation.show?(current_user, conversation) + # ConversationPart + defp can?(%User{} = current_user, :create, %ConversationPart{}, %{} = params), do: Policy.ConversationPart.create?(current_user, params) + defp can?(%User{} = current_user, :show, %ConversationPart{} = conversation_part, %{}), do: Policy.ConversationPart.show?(current_user, conversation_part) + # DonationGoal defp can?(%User{} = current_user, :create, %DonationGoal{}, %{} = params), do: Policy.DonationGoal.create?(current_user, params) defp can?(%User{} = current_user, :update, %DonationGoal{} = donation_goal, %{}), do: Policy.DonationGoal.update?(current_user, donation_goal) diff --git a/lib/code_corps_web/controllers/conversation_part_controller.ex b/lib/code_corps_web/controllers/conversation_part_controller.ex new file mode 100644 index 000000000..f48c51209 --- /dev/null +++ b/lib/code_corps_web/controllers/conversation_part_controller.ex @@ -0,0 +1,48 @@ +defmodule CodeCorpsWeb.ConversationPartController do + @moduledoc false + use CodeCorpsWeb, :controller + + alias CodeCorps.{ + ConversationPart, + Messages, + User + } + + action_fallback CodeCorpsWeb.FallbackController + plug CodeCorpsWeb.Plug.DataToAttributes + plug CodeCorpsWeb.Plug.IdsToIntegers + + @spec index(Conn.t, map) :: Conn.t + def index(%Conn{} = conn, %{} = params) do + with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource, + conversation_parts <- ConversationPart |> Policy.scope(current_user) |> Messages.list_parts(params) do + conn |> render("index.json-api", data: conversation_parts) + end + end + + @spec create(Plug.Conn.t, map) :: Conn.t + def create(%Conn{} = conn, %{} = params) do + with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource, + {:ok, :authorized} <- current_user |> Policy.authorize(:create, %ConversationPart{}, params), + {:ok, %ConversationPart{} = message} <- Messages.add_part(params), + message <- preload(message) + do + conn |> put_status(:created) |> render("show.json-api", data: message) + end + end + + @spec show(Conn.t, map) :: Conn.t + def show(%Conn{} = conn, %{"id" => id}) do + with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource, + %ConversationPart{} = conversation_part <- Messages.get_part(id), + {:ok, :authorized} <- current_user |> Policy.authorize(:show, conversation_part, %{}) do + conn |> render("show.json-api", data: conversation_part) + end + end + + @preloads [:author, :conversation] + + def preload(data) do + Repo.preload(data, @preloads) + end +end diff --git a/lib/code_corps_web/router.ex b/lib/code_corps_web/router.ex index 10ebe8fbf..4f1183a0d 100644 --- a/lib/code_corps_web/router.ex +++ b/lib/code_corps_web/router.ex @@ -72,6 +72,7 @@ defmodule CodeCorpsWeb.Router do resources "/categories", CategoryController, only: [:create, :update] resources "/comments", CommentController, only: [:create, :update] resources "/conversations", ConversationController, only: [:index, :show] + resources "/conversation-parts", ConversationPartController, only: [:index, :show, :create] resources "/donation-goals", DonationGoalController, only: [:create, :update, :delete] post "/oauth/github", UserController, :github_oauth resources "/github-app-installations", GithubAppInstallationController, only: [:create] diff --git a/lib/code_corps_web/views/conversation_part_view.ex b/lib/code_corps_web/views/conversation_part_view.ex new file mode 100644 index 000000000..ed0d50054 --- /dev/null +++ b/lib/code_corps_web/views/conversation_part_view.ex @@ -0,0 +1,10 @@ +defmodule CodeCorpsWeb.ConversationPartView do + @moduledoc false + use CodeCorpsWeb, :view + use JaSerializer.PhoenixView + + attributes [:body, :inserted_at, :read_at, :updated_at] + + has_one :author, type: "user", field: :author_id + has_one :conversation, type: "conversation", field: :conversation_id +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index fe80c6ef0..9691dab1f 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -4159,3 +4159,4 @@ ALTER TABLE ONLY users -- INSERT INTO "schema_migrations" (version) VALUES (20160723215749), (20160804000000), (20160804001111), (20160805132301), (20160805203929), (20160808143454), (20160809214736), (20160810124357), (20160815125009), (20160815143002), (20160816020347), (20160816034021), (20160817220118), (20160818000944), (20160818132546), (20160820113856), (20160820164905), (20160822002438), (20160822004056), (20160822011624), (20160822020401), (20160822044612), (20160830081224), (20160830224802), (20160911233738), (20160912002705), (20160912145957), (20160918003206), (20160928232404), (20161003185918), (20161019090945), (20161019110737), (20161020144622), (20161021131026), (20161031001615), (20161121005339), (20161121014050), (20161121043941), (20161121045709), (20161122015942), (20161123081114), (20161123150943), (20161124085742), (20161125200620), (20161126045705), (20161127054559), (20161205024856), (20161207112519), (20161209192504), (20161212005641), (20161214005935), (20161215052051), (20161216051447), (20161218005913), (20161219160401), (20161219163909), (20161220141753), (20161221085759), (20161226213600), (20161231063614), (20170102130055), (20170102181053), (20170104113708), (20170104212623), (20170104235423), (20170106013143), (20170115035159), (20170115230549), (20170121014100), (20170131234029), (20170201014901), (20170201025454), (20170201035458), (20170201183258), (20170220032224), (20170224233516), (20170226050552), (20170228085250), (20170308214128), (20170308220713), (20170308222552), (20170313130611), (20170318032449), (20170318082740), (20170324194827), (20170424215355), (20170501225441), (20170505224222), (20170526095401), (20170602000208), (20170622205732), (20170626231059), (20170628092119), (20170628213609), (20170629183404), (20170630140136), (20170706132431), (20170707213648), (20170711122252), (20170717092127), (20170725060612), (20170727052644), (20170731130121), (20170814131722), (20170913114958), (20170921014405), (20170925214512), (20170925230419), (20170926134646), (20170927100300), (20170928234412), (20171003134956), (20171003225853), (20171006063358), (20171006161407), (20171012215106), (20171012221231), (20171016125229), (20171016125516), (20171016223356), (20171016235656), (20171017235433), (20171019191035), (20171025184225), (20171026010933), (20171027061833), (20171028011642), (20171028173508), (20171030182857), (20171031232023), (20171031234356), (20171101023309), (20171104013543), (20171106045740), (20171106050209), (20171106103153), (20171106200036), (20171109231538), (20171110001134), (20171114010851), (20171114033357), (20171114225214), (20171114225713), (20171114232534), (20171115201624), (20171115225358), (20171119004204), (20171121075226), (20171121144138), (20171123065902), (20171127215847), (20171201073818), (20171205161052), (20171213062707); + diff --git a/test/lib/code_corps/messages/conversation_parts_test.exs b/test/lib/code_corps/messages/conversation_parts_test.exs new file mode 100644 index 000000000..947e20493 --- /dev/null +++ b/test/lib/code_corps/messages/conversation_parts_test.exs @@ -0,0 +1,67 @@ +defmodule CodeCorps.Messages.ConversationPartsTest do + use CodeCorps.ModelCase + + alias CodeCorps.{ + ConversationPart, + Messages.ConversationParts, + Repo + } + + @valid_attrs %{ + body: "Test body." + } + + describe "create_changeset/2" do + test "with valid attributes" do + attrs = @valid_attrs |> Map.merge(%{author_id: 1, conversation_id: 1}) + changeset = ConversationParts.create_changeset(%ConversationPart{}, attrs) + assert changeset.valid? + end + + test "requires author_id" do + conversation_id = insert(:conversation).id + + changeset = ConversationParts.create_changeset(%ConversationPart{}, %{conversation_id: conversation_id}) + + refute changeset.valid? + assert_error_message(changeset, :author_id, "can't be blank") + end + + test "requires conversation_id" do + author_id = insert(:user).id + + changeset = ConversationParts.create_changeset(%ConversationPart{}, %{author_id: author_id}) + + refute changeset.valid? + assert_error_message(changeset, :conversation_id, "can't be blank") + end + + test "requires id of actual author" do + author_id = -1 + conversation_id = insert(:conversation).id + attrs = @valid_attrs |> Map.merge(%{author_id: author_id, conversation_id: conversation_id}) + + {result, changeset} = + ConversationParts.create_changeset(%ConversationPart{}, attrs) + |> Repo.insert() + + assert result == :error + refute changeset.valid? + assert_error_message(changeset, :author, "does not exist") + end + + test "requires id of actual conversation" do + author_id = insert(:user).id + conversation_id = -1 + attrs = @valid_attrs |> Map.merge(%{author_id: author_id, conversation_id: conversation_id}) + + {result, changeset} = + ConversationParts.create_changeset(%ConversationPart{}, attrs) + |> Repo.insert() + + assert result == :error + refute changeset.valid? + assert_error_message(changeset, :conversation, "does not exist") + end + end +end diff --git a/test/lib/code_corps/messages/messages_test.exs b/test/lib/code_corps/messages/messages_test.exs index c891ddf7d..898d053d5 100644 --- a/test/lib/code_corps/messages/messages_test.exs +++ b/test/lib/code_corps/messages/messages_test.exs @@ -5,7 +5,7 @@ defmodule CodeCorps.MessagesTest do import Ecto.Query, only: [where: 2] - alias CodeCorps.{Conversation, Message, Messages} + alias CodeCorps.{Conversation, ConversationPart, Message, Messages} defp get_and_sort_ids(records) do records |> Enum.map(&Map.get(&1, :id)) |> Enum.sort @@ -283,6 +283,13 @@ defmodule CodeCorps.MessagesTest do end end + describe "list_parts/2" do + test "returns all records by default" do + insert_list(3, :conversation_part) + assert ConversationPart |> Messages.list_parts(%{}) |> Enum.count == 3 + end + end + describe "get_conversation/1" do test "gets a single conversation" do conversation = insert(:conversation) @@ -292,4 +299,32 @@ defmodule CodeCorps.MessagesTest do assert result.id == conversation.id end end + + describe "get_part/1" do + test "gets a single part" do + conversation_part = insert(:conversation_part) + + result = Messages.get_part(conversation_part.id) + + assert result.id == conversation_part.id + end + end + + describe "add_part/1" do + test "creates a conversation part" do + conversation = insert(:conversation) + user = insert(:user) + attrs = %{ + author_id: user.id, + body: "Test body", + conversation_id: conversation.id + } + + {:ok, %ConversationPart{} = conversation_part} = Messages.add_part(attrs) + + assert conversation_part.author_id == user.id + assert conversation_part.body == "Test body" + assert conversation_part.conversation_id == conversation.id + end + end end diff --git a/test/lib/code_corps/policy/conversation_part_test.exs b/test/lib/code_corps/policy/conversation_part_test.exs new file mode 100644 index 000000000..3b94b19fd --- /dev/null +++ b/test/lib/code_corps/policy/conversation_part_test.exs @@ -0,0 +1,183 @@ +defmodule CodeCorps.Policy.ConversationPartTest do + use CodeCorps.PolicyCase + + import CodeCorps.Policy.ConversationPart, only: [create?: 2, scope: 2, show?: 2] + + alias CodeCorps.{ConversationPart, Repo} + + defp params(user, conversation) do + %{ + "author_id" => user.id, + "body" => "Test", + "conversation_id" => conversation.id + } + end + + describe "scope" do + test "returns all records for admin user" do + insert_list(3, :conversation_part) + user = insert(:user, admin: true) + + assert ConversationPart |> scope(user) |> Repo.all |> Enum.count == 3 + end + + test "returns records where user is the author or they administer the project" do + user = insert(:user, admin: false) + + %{project: project_user_applied_to} = + insert(:project_user, user: user, role: "pending") + + %{project: project_user_contributes_to} = + insert(:project_user, user: user, role: "contributor") + + %{project: project_user_administers} = + insert(:project_user, user: user, role: "admin") + + %{project: project_user_owns} = + insert(:project_user, user: user, role: "owner") + + message_in_project_applied_to = + insert(:message, project: project_user_applied_to) + + message_in_contributing_project = + insert(:message, project: project_user_contributes_to) + + message_in_administered_project = + insert(:message, project: project_user_administers) + + message_in_owned_project = + insert(:message, project: project_user_owns) + + conversation_when_target = insert(:conversation, user: user) + conversation_when_pending = + insert(:conversation, message: message_in_project_applied_to) + conversation_when_contributor = + insert(:conversation, message: message_in_contributing_project) + conversation_when_admin = + insert(:conversation, message: message_in_administered_project) + conversation_when_owner = + insert(:conversation, message: message_in_owned_project) + some_other_conversation = insert(:conversation) + + part_in_conversation_when_target = + insert(:conversation_part, conversation: conversation_when_target) + part_in_project_applied_to = + insert(:conversation_part, conversation: conversation_when_pending) + part_in_contributing_project = + insert(:conversation_part, conversation: conversation_when_contributor) + part_in_administered_project = + insert(:conversation_part, conversation: conversation_when_admin) + part_in_owned_project = + insert(:conversation_part, conversation: conversation_when_owner) + part_in_some_other_conversation = + insert(:conversation_part, conversation: some_other_conversation) + + result_ids = + ConversationPart + |> scope(user) + |> Repo.all + |> Enum.map(&Map.get(&1, :id)) + + assert part_in_conversation_when_target.id in result_ids + refute part_in_project_applied_to.id in result_ids + refute part_in_contributing_project.id in result_ids + assert part_in_administered_project.id in result_ids + assert part_in_owned_project.id in result_ids + refute part_in_some_other_conversation.id in result_ids + end + end + + describe "create?" do + test "returns true when user is the target" do + user = insert(:user) + message = insert(:message) + conversation = insert(:conversation, message: message, user: user) + params = params(user, conversation) + + assert create?(user, params) + end + + test "returns false when user is a pending project member" do + %{project: project, user: user} = insert(:project_user, role: "pending") + message = insert(:message, project: project) + conversation = insert(:conversation, message: message) + params = params(user, conversation) + + refute create?(user, params) + end + + test "returns false when user is a project contributor" do + %{project: project, user: user} = insert(:project_user, role: "contributor") + message = insert(:message, project: project) + conversation = insert(:conversation, message: message) + params = params(user, conversation) + + refute create?(user, params) + end + + test "returns true when user is a project admin" do + %{project: project, user: user} = insert(:project_user, role: "admin") + message = insert(:message, project: project) + conversation = insert(:conversation, message: message) + params = params(user, conversation) + + assert create?(user, params) + end + + test "returns true when user is project owner" do + %{project: project, user: user} = insert(:project_user, role: "owner") + message = insert(:message, project: project) + conversation = insert(:conversation, message: message) + params = params(user, conversation) + + assert create?(user, params) + end + end + + describe "show?" do + test "returns true when user is the target" do + user = insert(:user) + message = insert(:message) + conversation = insert(:conversation, message: message, user: user) + conversation_part = insert(:conversation_part, conversation: conversation) + + assert show?(user, conversation_part) + end + + test "returns false when user is a pending project member" do + %{project: project, user: user} = insert(:project_user, role: "pending") + message = insert(:message, project: project) + conversation = insert(:conversation, message: message) + conversation_part = insert(:conversation_part, conversation: conversation) + + refute show?(user, conversation_part) + end + + test "returns false when user is a project contributor" do + %{project: project, user: user} = insert(:project_user, role: "contributor") + message = insert(:message, project: project) + conversation = insert(:conversation, message: message) + conversation_part = insert(:conversation_part, conversation: conversation) + + refute show?(user, conversation_part) + end + + test "returns true when user is a project admin" do + %{project: project, user: user} = insert(:project_user, role: "admin") + message = insert(:message, project: project) + conversation = insert(:conversation, message: message) + conversation_part = insert(:conversation_part, conversation: conversation) + + assert show?(user, conversation_part) + end + + test "returns true when user is project owner" do + %{project: project, user: user} = insert(:project_user, role: "owner") + message = insert(:message, project: project) + conversation = insert(:conversation, message: message) + conversation_part = insert(:conversation_part, conversation: conversation) + + assert show?(user, conversation_part) + end + end +end diff --git a/test/lib/code_corps/helpers/policy_test.exs b/test/lib/code_corps/policy/helpers_test.exs similarity index 90% rename from test/lib/code_corps/helpers/policy_test.exs rename to test/lib/code_corps/policy/helpers_test.exs index bd4ee97aa..bea4090e2 100644 --- a/test/lib/code_corps/helpers/policy_test.exs +++ b/test/lib/code_corps/policy/helpers_test.exs @@ -105,6 +105,28 @@ defmodule CodeCorps.Policy.HelpersTest do end end + describe "get_conversation/1" do + test "should return conversation of a map" do + conversation = insert(:conversation) + result = Helpers.get_conversation(%{"conversation_id" => conversation.id}) + assert result.id == conversation.id + end + + test "should return conversation of a ConversationPart" do + conversation = insert(:conversation) + conversation_part = insert(:conversation_part, conversation: conversation) + result = Helpers.get_conversation(conversation_part) + assert result.id == conversation.id + end + + test "should return conversation of a Changeset" do + conversation = insert(:conversation) + changeset = %Changeset{changes: %{conversation_id: conversation.id}} + result = Helpers.get_conversation(changeset) + assert result.id == conversation.id + end + end + describe "get_organization/1" do test "return organization if the organization_id is defined on the struct" do organization = insert(:organization) diff --git a/test/lib/code_corps_web/controllers/conversation_part_controller_test.exs b/test/lib/code_corps_web/controllers/conversation_part_controller_test.exs new file mode 100644 index 000000000..f9537967d --- /dev/null +++ b/test/lib/code_corps_web/controllers/conversation_part_controller_test.exs @@ -0,0 +1,103 @@ +defmodule CodeCorpsWeb.ConversationPartControllerTest do + use CodeCorpsWeb.ApiCase, resource_name: :conversation_part + + @valid_attrs %{ + body: "Test body." + } + + @invalid_attrs %{ + body: nil + } + + describe "index" do + @tag :authenticated + test "lists all entries user is authorized to view", %{conn: conn, current_user: user} do + %{project: project} = insert(:project_user, role: "admin", user: user) + message_on_user_administered_project = insert(:message, project: project) + + conversation_on_user_administered_project = + insert(:conversation, message: message_on_user_administered_project) + conversation_part_in_project = + insert(:conversation_part, conversation: conversation_on_user_administered_project) + + conversation_by_user = insert(:conversation, user: user) + conversation_part_from_user = + insert(:conversation_part, conversation: conversation_by_user) + + other_conversation = insert(:conversation) + _other_part = insert(:conversation_part, conversation: other_conversation) + + conn + |> request_index + |> json_response(200) + |> assert_ids_from_response([ + conversation_part_in_project.id, + conversation_part_from_user.id + ]) + end + + @tag authenticated: :admin + test "lists all entries if user is admin", %{conn: conn} do + [part_1, part_2] = insert_pair(:conversation_part) + + conn + |> request_index + |> json_response(200) + |> assert_ids_from_response([part_1.id, part_2.id]) + end + end + + describe "show" do + @tag :authenticated + test "shows chosen resource", %{conn: conn, current_user: user} do + conversation = insert(:conversation, user: user) + conversation_part = insert(:conversation_part, conversation: conversation) + + conn + |> request_show(conversation_part) + |> json_response(200) + |> assert_id_from_response(conversation_part.id) + end + + test "renders 401 when unauthenticated", %{conn: conn} do + conversation_part = insert(:conversation_part) + assert conn |> request_show(conversation_part) |> json_response(401) + end + + @tag :authenticated + test "renders 403 when unauthorized", %{conn: conn} do + conversation_part = insert(:conversation_part) + assert conn |> request_show(conversation_part) |> json_response(403) + end + end + + describe "create" do + @tag :authenticated + test "creates and renders resource when data is valid", %{conn: conn, current_user: user} do + conversation = insert(:conversation, user: user) + attrs = @valid_attrs |> Map.merge(%{author_id: user.id, conversation_id: conversation.id}) + + assert conn |> request_create(attrs) |> json_response(201) + end + + @tag :authenticated + test "does not create resource and renders 422 when data is invalid", %{ + conn: conn, + current_user: user + } do + conversation = insert(:conversation, user: user) + attrs = @invalid_attrs |> Map.merge(%{author_id: user.id, conversation_id: conversation.id}) + + assert conn |> request_create(attrs) |> json_response(422) + end + + test "does not create resource and renders 401 when not authenticated", %{conn: conn} do + assert conn |> request_create |> json_response(401) + end + + @tag :authenticated + test "renders 403 when not authorized", %{conn: conn} do + assert conn |> request_create |> json_response(403) + end + end +end diff --git a/test/lib/code_corps_web/views/conversation_part_view_test.exs b/test/lib/code_corps_web/views/conversation_part_view_test.exs new file mode 100644 index 000000000..c953e4076 --- /dev/null +++ b/test/lib/code_corps_web/views/conversation_part_view_test.exs @@ -0,0 +1,42 @@ +defmodule CodeCorpsWeb.ConversationPartViewTest do + use CodeCorpsWeb.ViewCase + + test "renders all attributes and relationships properly" do + conversation_part = insert(:conversation_part) + + rendered_json = + render(CodeCorpsWeb.ConversationPartView, "show.json-api", data: conversation_part) + + expected_json = %{ + "data" => %{ + "id" => conversation_part.id |> Integer.to_string, + "type" => "conversation-part", + "attributes" => %{ + "body" => conversation_part.body, + "inserted-at" => conversation_part.inserted_at, + "read-at" => conversation_part.read_at, + "updated-at" => conversation_part.updated_at + }, + "relationships" => %{ + "author" => %{ + "data" => %{ + "id" => conversation_part.author_id |> Integer.to_string, + "type" => "user" + } + }, + "conversation" => %{ + "data" => %{ + "id" => conversation_part.conversation_id |> Integer.to_string, + "type" => "conversation" + } + } + } + }, + "jsonapi" => %{ + "version" => "1.0" + } + } + + assert rendered_json == expected_json + end +end