From 75466519fb10f67fbbd2163412483f6ae2e27af6 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 15 Dec 2017 15:04:23 +0100 Subject: [PATCH] Add support for creating associated conversations with message - Fix DataToAttributes plug's parse_included fn - Add conversation parts to view and controller - Add conversation channel --- config/dev.exs | 2 +- lib/code_corps/messages/conversation_parts.ex | 8 +- lib/code_corps/messages/conversations.ex | 19 +++ lib/code_corps/messages/messages.ex | 5 + .../channels/conversation_channel.ex | 34 +++++ lib/code_corps_web/channels/user_socket.ex | 11 +- .../controllers/conversation_controller.ex | 10 +- .../controllers/message_controller.ex | 8 +- .../plugs/data_to_attributes.ex | 37 ++++- lib/code_corps_web/views/conversation_view.ex | 2 + lib/code_corps_web/views/message_view.ex | 2 + priv/repo/structure.sql | 4 +- .../lib/code_corps/messages/messages_test.exs | 129 ++++++++++++++++ .../channels/conversation_channel_test.exs | 52 +++++++ .../controllers/message_controller_test.exs | 33 ++++ .../plugs/data_to_attributes_test.exs | 142 ++++++++++++++++++ .../views/conversation_view_test.exs | 27 +++- .../views/message_view_test.exs | 8 +- test/support/channel_case.ex | 7 +- test/support/json_api_helpers.ex | 5 +- 20 files changed, 514 insertions(+), 31 deletions(-) create mode 100644 lib/code_corps/messages/conversations.ex create mode 100644 lib/code_corps_web/channels/conversation_channel.ex create mode 100644 test/lib/code_corps_web/channels/conversation_channel_test.exs create mode 100644 test/lib/code_corps_web/plugs/data_to_attributes_test.exs diff --git a/config/dev.exs b/config/dev.exs index e55c5c3b9..439c45194 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -43,7 +43,7 @@ config :code_corps, CodeCorps.Repo, pool_size: 10 # CORS allowed origins -config :code_corps, allowed_origins: ["http://localhost:4200"] +config :code_corps, allowed_origins: ["http://localhost:4200", "chrome-extension://pfdhoblngboilpfeibdedpjgfnlcodoo"] config :code_corps, CodeCorps.Guardian, secret_key: "e62fb6e2746f6b1bf8b5b735ba816c2eae1d5d76e64f18f3fc647e308b0c159e" diff --git a/lib/code_corps/messages/conversation_parts.ex b/lib/code_corps/messages/conversation_parts.ex index f33c3cdaa..3d1b047a5 100644 --- a/lib/code_corps/messages/conversation_parts.ex +++ b/lib/code_corps/messages/conversation_parts.ex @@ -10,17 +10,21 @@ defmodule CodeCorps.Messages.ConversationParts do ConversationPart, Repo } + alias CodeCorpsWeb.ConversationChannel @spec create(map) :: ConversationPart.t | Ecto.Changeset.t def create(attrs) do - %ConversationPart{} |> create_changeset(attrs) |> Repo.insert() + with {:ok, %ConversationPart{} = conversation_part} <- %ConversationPart{} |> create_changeset(attrs) |> Repo.insert() do + ConversationChannel.broadcast_new_conversation_part(conversation_part) + {:ok, conversation_part} + end 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]) + |> cast(attrs, [:author_id, :body, :conversation_id]) |> validate_required([:author_id, :body, :conversation_id]) |> assoc_constraint(:author) |> assoc_constraint(:conversation) diff --git a/lib/code_corps/messages/conversations.ex b/lib/code_corps/messages/conversations.ex new file mode 100644 index 000000000..453ea13d7 --- /dev/null +++ b/lib/code_corps/messages/conversations.ex @@ -0,0 +1,19 @@ +defmodule CodeCorps.Messages.Conversations do + @moduledoc ~S""" + Subcontext aimed at managing `CodeCorps.Conversation` records aimed at a + specific user belonging to a `CodeCorps.Message`. + """ + + alias Ecto.Changeset + + alias CodeCorps.{Conversation} + + @doc false + @spec create_changeset(Conversation.t, map) :: Ecto.Changeset.t + def create_changeset(%Conversation{} = conversation, %{} = attrs) do + conversation + |> Changeset.cast(attrs, [:user_id]) + |> Changeset.validate_required([:user_id]) + |> Changeset.assoc_constraint(:user) + end +end diff --git a/lib/code_corps/messages/messages.ex b/lib/code_corps/messages/messages.ex index 4e6847167..7857c5c94 100644 --- a/lib/code_corps/messages/messages.ex +++ b/lib/code_corps/messages/messages.ex @@ -67,6 +67,11 @@ defmodule CodeCorps.Messages do def create(%{} = params) do %Message{} |> Message.changeset(params) + |> Changeset.cast(params, [:author_id, :project_id]) + |> Changeset.validate_required([:author_id, :project_id]) + |> Changeset.assoc_constraint(:author) + |> Changeset.assoc_constraint(:project) + |> Changeset.cast_assoc(:conversations, with: &Messages.Conversations.create_changeset/2) |> Repo.insert() end diff --git a/lib/code_corps_web/channels/conversation_channel.ex b/lib/code_corps_web/channels/conversation_channel.ex new file mode 100644 index 000000000..d5d24108f --- /dev/null +++ b/lib/code_corps_web/channels/conversation_channel.ex @@ -0,0 +1,34 @@ +defmodule CodeCorpsWeb.ConversationChannel do + use Phoenix.Channel + + alias CodeCorps.{Conversation, Policy, Repo, User} + alias Phoenix.Socket + + @spec join(String.t, map, Socket.t) :: {:ok, Socket.t} | {:error, map} + def join("conversation:" <> id, %{}, %Socket{} = socket) do + with %Conversation{} = conversation <- Conversation |> Repo.get(id), + %User{} = current_user <- socket.assigns[:current_user], + {:ok, :authorized} <- current_user |> Policy.authorize(:show, conversation, %{}) do + + {:ok, socket} + else + nil -> {:error, %{reason: "unauthenticated"}} + {:error, :not_authorized} -> {:error, %{reason: "unauthorized"}} + end + end + + def event("new:conversation-part", socket, message) do + broadcast socket, "new:conversation-part", message + {:ok, socket} + end + + def broadcast_new_conversation_part(conversation_part) do + channel = "conversation:#{conversation_part.conversation_id}" + event = "new:conversation-part" + payload = %{ + id: conversation_part.id + } + + CodeCorpsWeb.Endpoint.broadcast(channel, event, payload) + end +end diff --git a/lib/code_corps_web/channels/user_socket.ex b/lib/code_corps_web/channels/user_socket.ex index bed624a6d..4cdf76413 100644 --- a/lib/code_corps_web/channels/user_socket.ex +++ b/lib/code_corps_web/channels/user_socket.ex @@ -2,7 +2,7 @@ defmodule CodeCorpsWeb.UserSocket do use Phoenix.Socket ## Channels - # channel "room:*", CodeCorps.RoomChannel + channel "conversation:*", CodeCorpsWeb.ConversationChannel ## Transports transport :websocket, Phoenix.Transports.WebSocket, @@ -20,8 +20,13 @@ defmodule CodeCorpsWeb.UserSocket do # # See `Phoenix.Token` documentation for examples in # performing token verification on connect. - def connect(_params, socket) do - {:ok, socket} + def connect(%{"token" => token}, socket) do + with {:ok, claims} <- CodeCorps.Guardian.decode_and_verify(token), + {:ok, user} <- CodeCorps.Guardian.resource_from_claims(claims) do + {:ok, assign(socket, :current_user, user)} + else + _ -> {:ok, socket} + end end # Socket id's are topics that allow you to identify all sockets for a given user: diff --git a/lib/code_corps_web/controllers/conversation_controller.ex b/lib/code_corps_web/controllers/conversation_controller.ex index 23b758fa6..f040998df 100644 --- a/lib/code_corps_web/controllers/conversation_controller.ex +++ b/lib/code_corps_web/controllers/conversation_controller.ex @@ -15,7 +15,7 @@ defmodule CodeCorpsWeb.ConversationController do @spec index(Conn.t, map) :: Conn.t def index(%Conn{} = conn, %{} = params) do with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource, - conversations <- Conversation |> Policy.scope(current_user) |> Messages.list_conversations(params) do + conversations <- Conversation |> Policy.scope(current_user) |> Messages.list_conversations(params) |> preload() do conn |> render("index.json-api", data: conversations) end end @@ -23,9 +23,15 @@ defmodule CodeCorpsWeb.ConversationController do @spec show(Conn.t, map) :: Conn.t def show(%Conn{} = conn, %{"id" => id}) do with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource, - %Conversation{} = conversation <- Messages.get_conversation(id), + %Conversation{} = conversation <- Messages.get_conversation(id) |> preload(), {:ok, :authorized} <- current_user |> Policy.authorize(:show, conversation, %{}) do conn |> render("show.json-api", data: conversation) end end + + @preloads [:conversation_parts, :message, :user] + + def preload(data) do + Repo.preload(data, @preloads) + end end diff --git a/lib/code_corps_web/controllers/message_controller.ex b/lib/code_corps_web/controllers/message_controller.ex index f1747b6c9..34a49d4e4 100644 --- a/lib/code_corps_web/controllers/message_controller.ex +++ b/lib/code_corps_web/controllers/message_controller.ex @@ -9,13 +9,13 @@ defmodule CodeCorpsWeb.MessageController do } action_fallback CodeCorpsWeb.FallbackController - plug CodeCorpsWeb.Plug.DataToAttributes + plug CodeCorpsWeb.Plug.DataToAttributes, [includes_many: ~w(conversation)] 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, - messages <- Message |> Policy.scope(current_user) |> Messages.list(params) do + messages <- Message |> Policy.scope(current_user) |> Messages.list(params) |> preload() do conn |> render("index.json-api", data: messages) end end @@ -23,7 +23,7 @@ defmodule CodeCorpsWeb.MessageController do @spec show(Conn.t, map) :: Conn.t def show(%Conn{} = conn, %{"id" => id}) do with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource, - %Message{} = message <- Message |> Repo.get(id), + %Message{} = message <- Message |> Repo.get(id) |> preload(), {:ok, :authorized} <- current_user |> Policy.authorize(:show, message, %{}) do conn |> render("show.json-api", data: message) end @@ -40,7 +40,7 @@ defmodule CodeCorpsWeb.MessageController do end end - @preloads [:author, :project] + @preloads [:author, :project, :conversations] def preload(data) do Repo.preload(data, @preloads) diff --git a/lib/code_corps_web/plugs/data_to_attributes.ex b/lib/code_corps_web/plugs/data_to_attributes.ex index c6ca3bb41..7beeabb16 100644 --- a/lib/code_corps_web/plugs/data_to_attributes.ex +++ b/lib/code_corps_web/plugs/data_to_attributes.ex @@ -1,9 +1,11 @@ defmodule CodeCorpsWeb.Plug.DataToAttributes do @moduledoc ~S""" - Converts params in the JSON api "data" format into flat params convient for + Converts params in the JSON api format into flat params convient for changeset casting. - This is done using `JaSerializer.Params.to_attributes/1` + For base parameters, this is done using `JaSerializer.Params.to_attributes/1` + + For included records, this is done using custom code. """ alias Plug.Conn @@ -12,13 +14,38 @@ defmodule CodeCorpsWeb.Plug.DataToAttributes do def init(opts), do: opts @spec call(Conn.t, Keyword.t) :: Plug.Conn.t - def call(%Conn{params: %{"data" => data} = params} = conn, _opts) do + def call(%Conn{params: %{} = params} = conn, opts \\ []) do attributes = params |> Map.delete("data") - |> Map.merge(data |> JaSerializer.Params.to_attributes) + |> Map.delete("included") + |> Map.merge(params |> parse_data()) + |> Map.merge(params |> parse_included(opts)) conn |> Map.put(:params, attributes) end - def call(%Conn{} = conn, _opts), do: conn + + @spec parse_data(map) :: map + defp parse_data(%{"data" => data}), do: data |> JaSerializer.Params.to_attributes + defp parse_data(%{}), do: %{} + + @spec parse_included(map, Keyword.t) :: map + defp parse_included(%{"included" => included}, opts) do + included |> Enum.reduce(%{}, fn (%{"data" => %{"type" => type}} = params, parsed) -> + attributes = params |> parse_data() + + if opts |> Keyword.get(:includes_many, []) |> Enum.member?(type) do + # this is an explicitly specified has_many, + # update existing data by adding new record + pluralized_type = type |> Inflex.pluralize + parsed |> Map.update(pluralized_type, [attributes], fn data -> + data ++ [attributes] + end) + else + # this is a belongs to, put a new submap into payload + parsed |> Map.put(type, attributes) + end + end) + end + defp parse_included(%{}, _opts), do: %{} end diff --git a/lib/code_corps_web/views/conversation_view.ex b/lib/code_corps_web/views/conversation_view.ex index 33ea2ab89..90fb80fbc 100644 --- a/lib/code_corps_web/views/conversation_view.ex +++ b/lib/code_corps_web/views/conversation_view.ex @@ -7,4 +7,6 @@ defmodule CodeCorpsWeb.ConversationView do has_one :user, type: "user", field: :user_id has_one :message, type: "message", field: :message_id + + has_many :conversation_parts, serializer: CodeCorpsWeb.ConversationPartView, identifiers: :always end diff --git a/lib/code_corps_web/views/message_view.ex b/lib/code_corps_web/views/message_view.ex index ba2aed161..70fb09376 100644 --- a/lib/code_corps_web/views/message_view.ex +++ b/lib/code_corps_web/views/message_view.ex @@ -7,4 +7,6 @@ defmodule CodeCorpsWeb.MessageView do has_one :author, type: "user", field: :author_id has_one :project, type: "project", field: :project_id + + has_many :conversations, serializer: CodeCorpsWeb.ConversationView, identifiers: :always end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 9691dab1f..d50ccec7a 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 9.5.10 --- Dumped by pg_dump version 10.1 +-- Dumped from database version 10.0 +-- Dumped by pg_dump version 10.0 SET statement_timeout = 0; SET lock_timeout = 0; diff --git a/test/lib/code_corps/messages/messages_test.exs b/test/lib/code_corps/messages/messages_test.exs index 898d053d5..261d218f7 100644 --- a/test/lib/code_corps/messages/messages_test.exs +++ b/test/lib/code_corps/messages/messages_test.exs @@ -2,10 +2,12 @@ defmodule CodeCorps.MessagesTest do @moduledoc false use CodeCorps.DbAccessCase + use Phoenix.ChannelTest import Ecto.Query, only: [where: 2] alias CodeCorps.{Conversation, ConversationPart, Message, Messages} + alias Ecto.Changeset defp get_and_sort_ids(records) do records |> Enum.map(&Map.get(&1, :id)) |> Enum.sort @@ -326,5 +328,132 @@ defmodule CodeCorps.MessagesTest do assert conversation_part.body == "Test body" assert conversation_part.conversation_id == conversation.id end + + test "broadcasts event on phoenix channel" do + conversation = insert(:conversation) + user = insert(:user) + attrs = %{ + author_id: user.id, + body: "Test body", + conversation_id: conversation.id + } + + CodeCorpsWeb.Endpoint.subscribe("conversation:#{conversation.id}") + {:ok, %ConversationPart{id: id}} = Messages.add_part(attrs) + assert_broadcast("new:conversation-part", %{id: ^id}) + CodeCorpsWeb.Endpoint.unsubscribe("conversation:#{conversation.id}") + end + end + + describe "create/1" do + test "creates a message" do + %{project: project, user: user} = insert(:project_user, role: "admin") + params = %{ + author_id: user.id, + body: "Foo", + initiated_by: "admin", + project_id: project.id, + subject: "Bar" + } + + {:ok, %Message{} = message} = params |> Messages.create + + assert message |> Map.take(params |> Map.keys) == params + end + + test "creates a conversation if attributes are provided" do + %{project: project, user: user} = insert(:project_user, role: "admin") + recipient = insert(:user) + params = %{ + author_id: user.id, + body: "Foo", + conversations: [%{user_id: recipient.id}], + initiated_by: "admin", + project_id: project.id, + subject: "Bar" + } + + {:ok, %Message{} = message} = params |> Messages.create + + assert message |> Map.take(params |> Map.delete(:conversations) |> Map.keys) == params |> Map.delete(:conversations) + assert Conversation |> Repo.get_by(message_id: message.id, status: "open", user_id: recipient.id) + end + + test "requires author_id, body, initiated_by, project_id" do + {:error, %Changeset{} = changeset} = %{} |> Messages.create + + assert changeset.errors[:author_id] + assert changeset.errors[:body] + assert changeset.errors[:initiated_by] + assert changeset.errors[:project_id] + end + + test "requires subject if initiated by admin" do + {:error, %Changeset{} = changeset} = + %{initiated_by: "admin"} |> Messages.create + + assert changeset.errors[:subject] + end + + test "allows blank subject if initiated by user" do + {:error, %Changeset{} = changeset} = + %{initiated_by: "user"} |> Messages.create + + refute changeset.errors[:subject] + end + + test "fails on project validation if id invalid" do + user = insert(:user) + params = %{ + author_id: user.id, + body: "Foo", + initiated_by: "admin", + project_id: 1, + subject: "Bar" + } + + {:error, %Changeset{} = changeset} = params |> Messages.create + + assert changeset.errors[:project] + end + + test "fails on user validation if id invalid" do + project = insert(:project) + params = %{ + author_id: -1, + body: "Foo", + initiated_by: "admin", + project_id: project.id, + subject: "Bar" + } + + {:error, %Changeset{} = changeset} = params |> Messages.create + + assert changeset.errors[:author] + end + + test "requires conversation user_id" do + params = %{conversations: [%{}]} + {:error, %Changeset{} = changeset} = params |> Messages.create + conversation_changeset = changeset.changes.conversations |> List.first + + assert conversation_changeset.errors[:user_id] + end + + test "fails on conversation user validation if id invalid" do + %{project: project, user: user} = insert(:project_user, role: "admin") + params = %{ + author_id: user.id, + body: "Foo", + conversations: [%{user_id: -1}], + initiated_by: "admin", + project_id: project.id, + subject: "Bar" + } + + {:error, %Changeset{} = changeset} = params |> Messages.create + conversation_changeset = changeset.changes.conversations |> List.first + assert conversation_changeset.errors[:user] + end end end diff --git a/test/lib/code_corps_web/channels/conversation_channel_test.exs b/test/lib/code_corps_web/channels/conversation_channel_test.exs new file mode 100644 index 000000000..fcc6ba6b3 --- /dev/null +++ b/test/lib/code_corps_web/channels/conversation_channel_test.exs @@ -0,0 +1,52 @@ +defmodule CodeCorpsWeb.ConversationChannelTest do + use CodeCorpsWeb.ChannelCase + + alias CodeCorps.{Conversation, User} + alias CodeCorpsWeb.ConversationChannel + + def build_socket(%Conversation{id: id}, %User{} = current_user) do + "test" + |> socket(%{current_user: current_user}) + |> subscribe_and_join(ConversationChannel, "conversation:#{id}") + end + + describe "conversation:id" do + test "requires authentication" do + %{id: id} = insert(:conversation) + + response = + "test" + |> socket(%{}) + |> subscribe_and_join(ConversationChannel, "conversation:#{id}") + + assert response == {:error, %{reason: "unauthenticated"}} + end + + test "ensures current user is authorized for :show on resource" do + user = insert(:user) + %{id: id} = insert(:conversation) + + response = + "test" + |> socket(%{current_user: user}) + |> subscribe_and_join(ConversationChannel, "conversation:#{id}") + + assert response == {:error, %{reason: "unauthorized"}} + end + + test "broadcasts new conversation part" do + %{id: id, user: user} = conversation = insert(:conversation) + + {:ok, %{}, _socket} = + "test" + |> socket(%{current_user: user}) + |> subscribe_and_join(ConversationChannel, "conversation:#{id}") + + %{id: conversation_part_id} = conversation_part = + insert(:conversation_part, conversation: conversation) + ConversationChannel.broadcast_new_conversation_part(conversation_part) + + assert_broadcast("new:conversation-part", %{id: ^conversation_part_id}) + end + end +end diff --git a/test/lib/code_corps_web/controllers/message_controller_test.exs b/test/lib/code_corps_web/controllers/message_controller_test.exs index b5deb1f7c..0f16a1dd8 100644 --- a/test/lib/code_corps_web/controllers/message_controller_test.exs +++ b/test/lib/code_corps_web/controllers/message_controller_test.exs @@ -1,6 +1,8 @@ defmodule CodeCorpsWeb.MessageControllerTest do use CodeCorpsWeb.ApiCase, resource_name: :message + alias CodeCorps.{Conversation, Message, Repo} + @valid_attrs %{ body: "Test body.", initiated_by: "admin", @@ -67,6 +69,34 @@ defmodule CodeCorpsWeb.MessageControllerTest do attrs = @valid_attrs |> Map.merge(%{author_id: user.id, project_id: project.id}) assert conn |> request_create(attrs) |> json_response(201) + + assert Repo.get_by(Message, project_id: project.id, author_id: user.id) + end + + @tag :authenticated + test "creates child conversation if attributes for it are provided", %{conn: conn, current_user: user} do + project = insert(:project) + insert(:project_user, project: project, user: user, role: "owner") + + recipient = insert(:user) + + conversation_payload = + %{user_id: recipient.id} + |> CodeCorps.JsonAPIHelpers.build_json_payload("conversation") + + payload = + @valid_attrs + |> Map.merge(%{author_id: user.id, project_id: project.id}) + |> CodeCorps.JsonAPIHelpers.build_json_payload + |> Map.put("included", [conversation_payload]) + + path = conn |> message_path(:create) + assert conn |> post(path, payload) |> json_response(201) + + message = Repo.get_by(Message, project_id: project.id, author_id: user.id) + assert message + + assert Repo.get_by(Conversation, user_id: recipient.id, message_id: message.id) end @tag :authenticated @@ -79,15 +109,18 @@ defmodule CodeCorpsWeb.MessageControllerTest do attrs = @invalid_attrs |> Map.merge(%{author_id: user.id, project_id: project.id}) assert conn |> request_create(attrs) |> json_response(422) + refute Repo.one(Message) end test "does not create resource and renders 401 when not authenticated", %{conn: conn} do assert conn |> request_create |> json_response(401) + refute Repo.one(Message) end @tag :authenticated test "renders 403 when not authorized", %{conn: conn} do assert conn |> request_create |> json_response(403) + refute Repo.one(Message) end @tag :authenticated diff --git a/test/lib/code_corps_web/plugs/data_to_attributes_test.exs b/test/lib/code_corps_web/plugs/data_to_attributes_test.exs new file mode 100644 index 000000000..dd15b63a8 --- /dev/null +++ b/test/lib/code_corps_web/plugs/data_to_attributes_test.exs @@ -0,0 +1,142 @@ +defmodule CodeCorpsWeb.Plug.DataToAttributesTest do + use CodeCorpsWeb.ConnCase + + alias CodeCorpsWeb.Plug.DataToAttributes + + test "converts basic JSON API payload to params suitable for Ecto", %{conn: conn} do + payload = %{ + "id" => "1", + "data" => %{ + "attributes" => %{"foo" => "bar", "baz" => "bat"}, + "type" => "resource" + } + } + + converted_params = + conn + |> Map.put(:params, payload) + |> DataToAttributes.call + |> Map.get(:params) + + assert converted_params == %{ + "baz" => "bat", + "foo" => "bar", + "id" => "1", + "type" => "resource" + } + end + + test "converts belongs_to specified via identifier map into proper id", %{conn: conn} do + payload = %{ + "id" => "1", + "data" => %{ + "attributes" => %{"foo" => "bar"}, + "relationships" => %{ + "baz" => %{"data" => %{"id" => "2", "type" => "baz"}} + }, + "type" => "resource" + } + } + + converted_params = + conn + |> Map.put(:params, payload) + |> DataToAttributes.call + |> Map.get(:params) + + assert converted_params == %{ + "baz_id" => "2", + "foo" => "bar", + "id" => "1", + "type" => "resource" + } + end + + test "converts has_many specified via identifier maps into proper ids", %{conn: conn} do + payload = %{ + "id" => "1", + "data" => %{ + "attributes" => %{"foo" => "bar"}, + "relationships" => %{ + "baz" => %{"data" => [ + %{"id" => "2", "type" => "baz"}, + %{"id" => "3", "type" => "baz"} + ]} + }, + "type" => "resource" + } + } + + converted_params = + conn + |> Map.put(:params, payload) + |> DataToAttributes.call + |> Map.get(:params) + + assert converted_params == %{ + "baz_ids" => ["2", "3"], + "foo" => "bar", + "id" => "1", + "type" => "resource" + } + end + + test "converts included belongs_to into proper subpayload", %{conn: conn} do + payload = %{ + "id" => "1", + "data" => %{ + "attributes" => %{"foo" => "bar"}, + "type" => "resource" + }, + "included" => [ + %{"data" => %{"attributes" => %{"baz_foo" => "baz_bar"}, "type" => "baz"}} + ] + } + + converted_params = + conn + |> Map.put(:params, payload) + |> DataToAttributes.call + |> Map.get(:params) + + assert converted_params == %{ + "baz" => %{ + "baz_foo" => "baz_bar", + "type" => "baz" + }, + "foo" => "bar", + "id" => "1", + "type" => "resource" + } + end + + test "converts included has_many into proper subpayload", %{conn: conn} do + payload = %{ + "id" => "1", + "data" => %{ + "attributes" => %{"foo" => "bar"}, + "type" => "resource" + }, + "included" => [ + %{"data" => %{"attributes" => %{"baz_foo" => "baz_bar"}, "type" => "baz"}}, + %{"data" => %{"attributes" => %{"baz_foo_2" => "baz_bar_2"}, "type" => "baz"}} + ] + } + + converted_params = + conn + |> Map.put(:params, payload) + |> DataToAttributes.call([includes_many: ["baz"]]) + |> Map.get(:params) + + assert converted_params == %{ + "bazs" => [ + %{"baz_foo" => "baz_bar", "type" => "baz"}, + %{"baz_foo_2" => "baz_bar_2", "type" => "baz"}, + ], + "foo" => "bar", + "id" => "1", + "type" => "resource" + } + end +end diff --git a/test/lib/code_corps_web/views/conversation_view_test.exs b/test/lib/code_corps_web/views/conversation_view_test.exs index 4faecdb74..44d593bab 100644 --- a/test/lib/code_corps_web/views/conversation_view_test.exs +++ b/test/lib/code_corps_web/views/conversation_view_test.exs @@ -1,11 +1,18 @@ defmodule CodeCorpsWeb.ConversationViewTest do use CodeCorpsWeb.ViewCase + alias CodeCorps.Repo + test "renders all attributes and relationships properly" do conversation = insert(:conversation) + conversation_part = insert(:conversation_part, conversation: conversation) rendered_json = - render(CodeCorpsWeb.ConversationView, "show.json-api", data: conversation) + CodeCorpsWeb.ConversationView + |> render( + "show.json-api", + data: conversation |> Repo.preload(:conversation_parts) + ) expected_json = %{ "data" => %{ @@ -18,17 +25,25 @@ defmodule CodeCorpsWeb.ConversationViewTest do "updated-at" => conversation.updated_at }, "relationships" => %{ - "user" => %{ - "data" => %{ - "id" => conversation.user_id |> Integer.to_string, - "type" => "user" - } + "conversation-parts" => %{ + "data" => [ + %{ + "id" => conversation_part.id |> Integer.to_string, + "type" => "conversation-part" + } + ] }, "message" => %{ "data" => %{ "id" => conversation.message_id |> Integer.to_string, "type" => "message" } + }, + "user" => %{ + "data" => %{ + "id" => conversation.user_id |> Integer.to_string, + "type" => "user" + } } } }, diff --git a/test/lib/code_corps_web/views/message_view_test.exs b/test/lib/code_corps_web/views/message_view_test.exs index 3a582e4bf..6020e75f8 100644 --- a/test/lib/code_corps_web/views/message_view_test.exs +++ b/test/lib/code_corps_web/views/message_view_test.exs @@ -1,12 +1,15 @@ defmodule CodeCorpsWeb.MessageViewTest do use CodeCorpsWeb.ViewCase + alias CodeCorps.Repo + test "renders all attributes and relationships properly" do project = insert(:project) user = insert(:user) message = insert(:message, author: user, project: project) + conversation = insert(:conversation, message: message) - rendered_json = render(CodeCorpsWeb.MessageView, "show.json-api", data: message) + rendered_json = render(CodeCorpsWeb.MessageView, "show.json-api", data: message |> Repo.preload(:conversations)) expected_json = %{ "data" => %{ @@ -23,6 +26,9 @@ defmodule CodeCorpsWeb.MessageViewTest do "author" => %{ "data" => %{"id" => message.author_id |> Integer.to_string, "type" => "user"} }, + "conversations" => %{ + "data" => [%{"id" => conversation.id |> Integer.to_string, "type" => "conversation"}] + }, "project" => %{ "data" => %{"id" => message.project_id |> Integer.to_string, "type" => "project"} } diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex index f2f6c3411..ab08f06dd 100644 --- a/test/support/channel_case.ex +++ b/test/support/channel_case.ex @@ -20,11 +20,12 @@ defmodule CodeCorpsWeb.ChannelCase do # Import conveniences for testing with channels use Phoenix.ChannelTest - alias CodeCorps.Repo - import Ecto - import Ecto.Changeset + import CodeCorps.Factories + import CodeCorpsWeb.ChannelCase + import Ecto.Query + alias CodeCorps.Repo # The default endpoint for testing @endpoint CodeCorpsWeb.Endpoint diff --git a/test/support/json_api_helpers.ex b/test/support/json_api_helpers.ex index 34c0e6066..693295946 100644 --- a/test/support/json_api_helpers.ex +++ b/test/support/json_api_helpers.ex @@ -5,11 +5,12 @@ defmodule CodeCorps.JsonAPIHelpers do """ @spec build_json_payload(map) :: map - def build_json_payload(attrs = %{}) do + def build_json_payload(attrs = %{}, type \\ nil) do %{ "data" => %{ "attributes" => attrs |> build_attributes(), - "relationships" => attrs |> build_relationships() + "relationships" => attrs |> build_relationships(), + "type" => type } } end