diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index 231487ba..cbe20d8b 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -124,8 +124,8 @@ defmodule EpochtalkServer.Models.Board do Creates a new `Board` in the database """ @spec create(board_attrs :: map()) :: {:ok, board :: t()} | {:error, Ecto.Changeset.t()} - def create(board) do - board_cs = create_changeset(%Board{}, board) + def create(board_attrs) do + board_cs = create_changeset(%Board{}, board_attrs) case Repo.insert(board_cs) do {:ok, db_board} -> diff --git a/lib/epochtalk_server/models/board_mapping.ex b/lib/epochtalk_server/models/board_mapping.ex index 3b612bae..a2e556a4 100644 --- a/lib/epochtalk_server/models/board_mapping.ex +++ b/lib/epochtalk_server/models/board_mapping.ex @@ -112,7 +112,18 @@ defmodule EpochtalkServer.Models.BoardMapping do left_join: s in subquery(sticky_count_subquery), on: bm.board_id == s.board_id, select_merge: %{ - stats: mb, + stats: %{ + board_id: mb.board_id, + post_count: mb.post_count, + thread_count: mb.thread_count, + total_post: mb.total_post, + total_thread_count: mb.total_thread_count, + last_post_username: mb.last_post_username, + last_post_created_at: mb.last_post_created_at, + last_thread_id: mb.last_thread_id, + last_thread_title: mb.last_thread_title, + last_post_position: mb.last_post_position + }, thread: %{ last_thread_slug: t.slug, last_thread_post_count: t.post_count, diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index 8695f023..d6b246f1 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -69,7 +69,7 @@ defmodule EpochtalkServer.Models.Category do |> Map.put(:updated_at, now) category - |> cast(attrs, [:name, :viewable_by, :created_at, :updated_at]) + |> cast(attrs, [:name, :view_order, :viewable_by, :postable_by, :created_at, :updated_at]) end @doc """ @@ -79,7 +79,7 @@ defmodule EpochtalkServer.Models.Category do Ecto.Changeset.t() def update_for_board_mapping_changeset(category, attrs) do category - |> cast(attrs, [:id, :name, :view_order, :viewable_by]) + |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by]) |> unique_constraint(:id, name: :categories_pkey) end diff --git a/lib/epochtalk_server/models/metadata_board.ex b/lib/epochtalk_server/models/metadata_board.ex index c5700eea..25334629 100644 --- a/lib/epochtalk_server/models/metadata_board.ex +++ b/lib/epochtalk_server/models/metadata_board.ex @@ -1,8 +1,12 @@ defmodule EpochtalkServer.Models.MetadataBoard do use Ecto.Schema import Ecto.Changeset + import Ecto.Query alias EpochtalkServer.Repo alias EpochtalkServer.Models.Board + alias EpochtalkServer.Models.Post + alias EpochtalkServer.Models.Thread + alias EpochtalkServer.Models.User alias EpochtalkServer.Models.MetadataBoard @moduledoc """ @@ -85,4 +89,67 @@ defmodule EpochtalkServer.Models.MetadataBoard do @spec insert(metadata_board :: t()) :: {:ok, metadata_board :: t()} | {:error, Ecto.Changeset.t()} def insert(%MetadataBoard{} = metadata_board), do: Repo.insert(metadata_board) + + @doc """ + Queries then updates `MetadataBoard` info for the specified Board` + """ + @spec update_last_post_info(metadata_board :: t(), board_id :: non_neg_integer) :: t() + def update_last_post_info(metadata_board, board_id) do + # query most recent post in thread and it's authoring user's data + last_post_subquery = + from t in Thread, + left_join: p in Post, + on: t.id == p.thread_id, + left_join: u in User, + on: u.id == p.user_id, + where: t.board_id == ^board_id, + order_by: [desc: p.created_at], + select: %{ + thread_id: p.thread_id, + created_at: p.created_at, + username: u.username, + position: p.position + } + + # query most recent thread in board, join last post subquery + last_post_query = + from t in Thread, + left_join: p in Post, + on: p.thread_id == t.id, + left_join: lp in subquery(last_post_subquery), + on: p.thread_id == lp.thread_id, + where: t.board_id == ^board_id, + order_by: [desc: t.created_at], + limit: 1, + select: %{ + board_id: t.board_id, + thread_id: t.id, + title: p.content["title"], + username: lp.username, + created_at: lp.created_at, + position: lp.position + } + + # update board metadata using queried data + updated_metadata_board = + if lp = Repo.one(last_post_query) do + change(metadata_board, + last_post_username: lp.username, + last_post_created_at: lp.created_at, + last_thread_id: lp.thread_id, + last_thread_title: lp.title, + last_post_position: lp.position + ) + else + change(metadata_board, + last_post_username: nil, + last_post_created_at: nil, + last_thread_id: nil, + last_thread_title: nil, + last_post_position: nil + ) + end + + Repo.update!(updated_metadata_board) + end end diff --git a/lib/epochtalk_server/models/thread.ex b/lib/epochtalk_server/models/thread.ex index 36661bdd..afd4f25a 100644 --- a/lib/epochtalk_server/models/thread.ex +++ b/lib/epochtalk_server/models/thread.ex @@ -7,6 +7,7 @@ defmodule EpochtalkServer.Models.Thread do alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Thread alias EpochtalkServer.Models.MetadataThread + alias EpochtalkServer.Models.MetadataBoard alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.Poll alias EpochtalkServer.Models.Post @@ -243,6 +244,89 @@ defmodule EpochtalkServer.Models.Thread do end end + @doc """ + Moves `Thread` to the specified `Board` given a `thread_id` and `board_id` + """ + @spec move(thread_id :: non_neg_integer, board_id :: non_neg_integer) :: + {:ok, thread :: t()} | {:error, Ecto.Changeset.t()} + def move(thread_id, board_id) do + case Repo.transaction(fn -> + # query and lock thread for update, + thread_lock_query = + from t in Thread, + where: t.id == ^thread_id, + select: t, + lock: "FOR UPDATE" + + thread = Repo.one(thread_lock_query) + + # prevent moving board to same board or non existent board + if thread.board_id != board_id && Repo.get_by(Board, id: board_id) != nil do + # query old board and lock for update + old_board_lock_query = + from b in Board, + join: mb in MetadataBoard, + on: mb.board_id == b.id, + where: b.id == ^thread.board_id, + select: {b, mb}, + lock: "FOR UPDATE" + + # locked old_board, reference this when updating + {old_board, old_board_meta} = Repo.one(old_board_lock_query) + + # query new board and lock for update + new_board_lock_query = + from b in Board, + join: mb in MetadataBoard, + on: mb.board_id == b.id, + where: b.id == ^board_id, + select: {b, mb}, + lock: "FOR UPDATE" + + # locked new_board, reference this when updating + {new_board, new_board_meta} = Repo.one(new_board_lock_query) + + # update old_board, thread and post count + old_board + |> change( + thread_count: old_board.thread_count - 1, + post_count: old_board.post_count - thread.post_count + ) + |> Repo.update!() + + # update new_board, thread and post count + new_board + |> change( + thread_count: new_board.thread_count + 1, + post_count: new_board.post_count + thread.post_count + ) + |> Repo.update!() + + # update thread's original board_id with new_board's id + thread + |> change(board_id: new_board.id) + |> Repo.update!() + + # update last post metadata info of both the old board and new board + MetadataBoard.update_last_post_info(old_board_meta, old_board.id) + MetadataBoard.update_last_post_info(new_board_meta, new_board.id) + + # return old board data for reference + %{old_board_name: old_board.name, old_board_id: old_board.id} + else + Repo.rollback(:invalid_board_id) + end + end) do + # transaction success return purged thread data + {:ok, thread_data} -> + {:ok, thread_data} + + # some other error + {:error, cs} -> + {:error, cs} + end + end + @doc """ Sets boolean indicating if the specified `Thread` is sticky given a `Thread` id """ diff --git a/lib/epochtalk_server_web/controllers/thread.ex b/lib/epochtalk_server_web/controllers/thread.ex index 112e79d2..692d4bf5 100644 --- a/lib/epochtalk_server_web/controllers/thread.ex +++ b/lib/epochtalk_server_web/controllers/thread.ex @@ -512,6 +512,67 @@ defmodule EpochtalkServerWeb.Controllers.Thread do end end + @doc """ + Used to move a `Thread` + """ + def move(conn, attrs) do + with user <- Guardian.Plug.current_resource(conn), + thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true), + new_board_id <- Validate.cast(attrs, "new_board_id", :integer, required: true), + :ok <- ACL.allow!(conn, "threads.move"), + user_priority <- ACL.get_user_priority(conn), + {:can_read, {:ok, true}} <- + {:can_read, Board.get_read_access_by_thread_id(thread_id, user_priority)}, + {:can_write, {:ok, true}} <- + {:can_write, Board.get_write_access_by_thread_id(thread_id, user_priority)}, + {:is_active, true} <- + {:is_active, User.is_active?(user.id)}, + {:board_banned, {:ok, false}} <- + {:board_banned, BoardBan.banned_from_board?(user, thread_id: thread_id)}, + {:bypass_thread_owner, true} <- + {:bypass_thread_owner, can_authed_user_bypass_owner_on_thread_move(user, thread_id)}, + {:ok, old_board_data} <- Thread.move(thread_id, new_board_id) do + render(conn, :move, old_board_data: old_board_data) + else + {:can_read, {:ok, false}} -> + ErrorHelpers.render_json_error( + conn, + 403, + "Unauthorized, you do not have permission to read" + ) + + {:can_write, {:ok, false}} -> + ErrorHelpers.render_json_error( + conn, + 403, + "Unauthorized, you do not have permission to write" + ) + + {:bypass_thread_owner, false} -> + ErrorHelpers.render_json_error( + conn, + 403, + "Unauthorized, you do not have permission to move another user's thread" + ) + + {:board_banned, {:ok, true}} -> + ErrorHelpers.render_json_error(conn, 403, "Unauthorized, you are banned from this board") + + {:is_active, false} -> + ErrorHelpers.render_json_error( + conn, + 400, + "Account must be active to move thread" + ) + + {:error, data} -> + ErrorHelpers.render_json_error(conn, 400, data) + + _ -> + ErrorHelpers.render_json_error(conn, 400, "Error, cannot move thread") + end + end + @doc """ Used to convert `Thread` slug to id """ @@ -685,4 +746,18 @@ defmodule EpochtalkServerWeb.Controllers.Thread do true ) end + + defp can_authed_user_bypass_owner_on_thread_move(user, thread_id) do + post = Thread.get_first_post_data_by_id(thread_id) + + ACL.bypass_post_owner( + user, + post, + "threads.move", + "owner", + false, + true, + true + ) + end end diff --git a/lib/epochtalk_server_web/json/board_json.ex b/lib/epochtalk_server_web/json/board_json.ex index 15af9635..9f626a1f 100644 --- a/lib/epochtalk_server_web/json/board_json.ex +++ b/lib/epochtalk_server_web/json/board_json.ex @@ -90,9 +90,9 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do # flatten needed boards data board = board - |> Map.merge(to_map_remove_nil(board.board)) + |> Map.merge(remove_nil(board.board)) |> Map.merge( - to_map_remove_nil(board.stats) + remove_nil(board.stats) |> Map.delete(:id) ) |> Map.merge(board.thread) @@ -157,8 +157,8 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do # flatten needed boards data board = board - |> Map.merge(to_map_remove_nil(board.board)) - |> Map.merge(to_map_remove_nil(board.stats)) + |> Map.merge(remove_nil(board.board)) + |> Map.merge(remove_nil(board.stats)) |> Map.merge(board.thread) # delete unneeded properties @@ -204,11 +204,16 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do parent end - defp to_map_remove_nil(nil), do: %{} + defp remove_nil(nil), do: %{} - defp to_map_remove_nil(struct) do + defp remove_nil(struct) when is_struct(struct) do struct |> Map.from_struct() + |> remove_nil() + end + + defp remove_nil(map) when is_map(map) do + map |> Enum.reject(fn {_, v} -> is_nil(v) end) |> Map.new() end diff --git a/lib/epochtalk_server_web/json/thread_json.ex b/lib/epochtalk_server_web/json/thread_json.ex index b277f244..ea7c64a9 100644 --- a/lib/epochtalk_server_web/json/thread_json.ex +++ b/lib/epochtalk_server_web/json/thread_json.ex @@ -140,6 +140,19 @@ defmodule EpochtalkServerWeb.Controllers.ThreadJSON do def purge(%{thread: thread}), do: thread + @doc """ + Renders move `Thread`. + + iex> old_board_data = %{ + iex> old_board_id: 2, + iex> old_board_name: "General Discussion" + iex> } + iex> EpochtalkServerWeb.Controllers.ThreadJSON.move(%{old_board_data: old_board_data}) + old_board_data + """ + def move(%{old_board_data: old_board_data}), + do: old_board_data + @doc """ Renders `Thread` id for slug to id route. """ diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index f0641d97..79945658 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -48,6 +48,7 @@ defmodule EpochtalkServerWeb.Router do post "/threads", Thread, :create post "/threads/:thread_id/lock", Thread, :lock post "/threads/:thread_id/sticky", Thread, :sticky + post "/threads/:thread_id/move", Thread, :move delete "/threads/:thread_id", Thread, :purge post "/threads/:thread_id/polls/vote", Poll, :vote delete "/threads/:thread_id/polls/vote", Poll, :delete_vote diff --git a/test/epochtalk_server/models/board_mapping_test.exs b/test/epochtalk_server/models/board_mapping_test.exs index 45747519..4fb6608e 100644 --- a/test/epochtalk_server/models/board_mapping_test.exs +++ b/test/epochtalk_server/models/board_mapping_test.exs @@ -9,11 +9,11 @@ defmodule Test.EpochtalkServer.Models.BoardMapping do end test "gets board mappings" do - category = insert(:category) - category_board1 = insert(:board) - category_board2 = insert(:board) - child_board1 = insert(:board) - child_board2 = insert(:board) + category = build(:category) + category_board1 = build(:board) + category_board2 = build(:board) + child_board1 = build(:board) + child_board2 = build(:board) build(:board_mapping, attributes: [ @@ -66,8 +66,8 @@ defmodule Test.EpochtalkServer.Models.BoardMapping do describe "update/1" do test "updates a board mapping" do - category = insert(:category) - board = insert(:board) + category = build(:category) + board = build(:board) result = build(:board_mapping, @@ -87,8 +87,8 @@ defmodule Test.EpochtalkServer.Models.BoardMapping do test "deletes a board mapping" do assert BoardMapping.all() |> Enum.count() == 0 - category = insert(:category) - board = insert(:board) + category = build(:category) + board = build(:board) build(:board_mapping, attributes: [ diff --git a/test/epochtalk_server/models/mention_test.exs b/test/epochtalk_server/models/mention_test.exs index b4b4953c..d064875a 100644 --- a/test/epochtalk_server/models/mention_test.exs +++ b/test/epochtalk_server/models/mention_test.exs @@ -4,8 +4,8 @@ defmodule Test.EpochtalkServer.Models.Mention do alias EpochtalkServer.Models.Mention setup %{users: %{user: user}} do - category = insert(:category) - board = insert(:board) + category = build(:category) + board = build(:board) build(:board_mapping, attributes: [ diff --git a/test/epochtalk_server/session_test.exs b/test/epochtalk_server/session_test.exs index 48130c9a..cae96e6c 100644 --- a/test/epochtalk_server/session_test.exs +++ b/test/epochtalk_server/session_test.exs @@ -518,7 +518,7 @@ defmodule Test.EpochtalkServer.Session do assert Map.get(resource_user, :moderating) == nil # create board and add user as moderator - board = insert(:board) + board = build(:board) BoardModerator.add_moderators_by_username(board.id, [authed_user.username]) # check session user updates with moderating diff --git a/test/epochtalk_server_web/controllers/board_test.exs b/test/epochtalk_server_web/controllers/board_test.exs index 7e403c06..8f834fe2 100644 --- a/test/epochtalk_server_web/controllers/board_test.exs +++ b/test/epochtalk_server_web/controllers/board_test.exs @@ -4,11 +4,11 @@ defmodule Test.EpochtalkServerWeb.Controllers.Board do alias EpochtalkServerWeb.CustomErrors.InvalidPermission setup %{conn: conn} do - category = insert(:category) - parent_board = insert(:board) - child_board = insert(:board) - admin_board = insert(:board, viewable_by: 1) - super_admin_board = insert(:board, viewable_by: 0) + category = build(:category) + parent_board = build(:board) + child_board = build(:board) + admin_board = build(:board, viewable_by: 1) + super_admin_board = build(:board, viewable_by: 0) build(:board_mapping, attributes: [ diff --git a/test/epochtalk_server_web/controllers/breadcrumb_test.exs b/test/epochtalk_server_web/controllers/breadcrumb_test.exs index a98ec887..8bd7c77e 100644 --- a/test/epochtalk_server_web/controllers/breadcrumb_test.exs +++ b/test/epochtalk_server_web/controllers/breadcrumb_test.exs @@ -3,10 +3,10 @@ defmodule Test.EpochtalkServerWeb.Controllers.Breadcrumb do import Test.Support.Factory setup %{users: %{user: user}} do - category = insert(:category) - parent_board = insert(:board) - child_board = insert(:board) - board_no_parent = insert(:board) + category = build(:category) + parent_board = build(:board) + child_board = build(:board) + board_no_parent = build(:board) build(:board_mapping, attributes: [ diff --git a/test/epochtalk_server_web/controllers/moderation_log_test.exs b/test/epochtalk_server_web/controllers/moderation_log_test.exs index 6206f48d..6fb13dfb 100644 --- a/test/epochtalk_server_web/controllers/moderation_log_test.exs +++ b/test/epochtalk_server_web/controllers/moderation_log_test.exs @@ -102,7 +102,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do users: %{user: user, admin_user: admin_user} } do mod_address = "127.0.0.2" - board = insert(:board) + board = build(:board) attrs = %{ mod: %{username: admin_user.username, id: 1, ip: mod_address}, @@ -148,7 +148,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do conn: conn, users: %{user: user} } do - board = insert(:board) + board = build(:board) factory_moderation_log = build(:moderation_log, %{ @@ -174,7 +174,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do conn: conn, users: %{user: user} } do - board = insert(:board) + board = build(:board) factory_moderation_log = build(:moderation_log, %{ @@ -832,7 +832,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do conn: conn, users: %{user: user} } do - board = insert(:board) + board = build(:board) factory_moderation_log = build(:moderation_log, %{ @@ -857,7 +857,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do conn: conn, users: %{user: user} } do - board = insert(:board) + board = build(:board) factory_moderation_log = build(:moderation_log, %{ @@ -879,7 +879,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'boards.create', gets page", %{conn: conn} do - board = insert(:board) + board = build(:board) factory_moderation_log = build(:moderation_log, %{ @@ -901,7 +901,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'boards.update', gets page", %{conn: conn} do - board = insert(:board) + board = build(:board) factory_moderation_log = build(:moderation_log, %{ @@ -923,7 +923,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'boards.delete', gets page", %{conn: conn} do - board = insert(:board) + board = build(:board) factory_moderation_log = build(:moderation_log, %{ @@ -945,7 +945,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'threads.title', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_id = thread.post.thread_id thread_slug = thread.attributes["slug"] @@ -972,7 +972,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'threads.lock', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_id = thread.post.thread_id thread_title = thread.post.content["title"] @@ -999,7 +999,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'threads.sticky', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_id = thread.post.thread_id thread_title = thread.post.content["title"] @@ -1026,7 +1026,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'threads.move', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_id = thread.post.thread_id thread_title = thread.post.content["title"] @@ -1060,7 +1060,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'threads.purge', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_title = thread.post.content["title"] @@ -1088,7 +1088,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'threads.editPoll', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_id = thread.post.thread_id thread_title = thread.post.content["title"] @@ -1118,7 +1118,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do conn: conn, users: %{user: user} } do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_id = thread.post.thread_id thread_title = thread.post.content["title"] @@ -1145,7 +1145,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'threads.lockPoll', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_id = thread.post.thread_id thread_title = thread.post.content["title"] @@ -1172,7 +1172,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'posts.update', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_title = thread.post.content["title"] thread_slug = thread.attributes["slug"] @@ -1199,7 +1199,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'posts.delete', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_title = thread.post.content["title"] thread_slug = thread.attributes["slug"] @@ -1226,7 +1226,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'posts.undelete', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_title = thread.post.content["title"] thread_slug = thread.attributes["slug"] @@ -1253,7 +1253,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do @tag authenticated: :admin test "when action_type is 'posts.purge', gets page", %{conn: conn, users: %{user: user}} do - board = insert(:board) + board = build(:board) thread = build(:thread, board: board, user: user) thread_id = thread.post.thread_id thread_title = thread.post.content["title"] @@ -1467,7 +1467,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do conn: conn, users: %{user: user} } do - board = insert(:board) + board = build(:board) factory_moderation_log = build(:moderation_log, %{ diff --git a/test/epochtalk_server_web/controllers/notification_test.exs b/test/epochtalk_server_web/controllers/notification_test.exs index 3044d68f..b50e61e1 100644 --- a/test/epochtalk_server_web/controllers/notification_test.exs +++ b/test/epochtalk_server_web/controllers/notification_test.exs @@ -1,12 +1,12 @@ defmodule Test.EpochtalkServerWeb.Controllers.Notification do - use Test.Support.ConnCase, async: false + use Test.Support.ConnCase, async: true import Test.Support.Factory @mentions_count 99 setup %{users: %{user: user, admin_user: admin_user}} do - board = insert(:board) - category = insert(:category) + board = build(:board) + category = build(:category) build(:board_mapping, attributes: [ @@ -19,8 +19,8 @@ defmodule Test.EpochtalkServerWeb.Controllers.Notification do mentions = build_list(@mentions_count, :mention, %{ - thread_id: thread_data.post.id, - post_id: thread_data.post.thread_id, + thread_id: thread_data.post.thread_id, + post_id: thread_data.post.id, mentioner_id: user.id, mentionee_id: admin_user.id }) @@ -67,8 +67,8 @@ defmodule Test.EpochtalkServerWeb.Controllers.Notification do test "when authenticated as notification receiver and notifications exceed default max, returns max+", %{conn: conn, users: %{user: user, admin_user: admin_user}, thread_data: thread_data} do build(:mention, %{ - thread_id: thread_data.post.id, - post_id: thread_data.post.thread_id, + thread_id: thread_data.post.thread_id, + post_id: thread_data.post.id, mentioner_id: user.id, mentionee_id: admin_user.id }) diff --git a/test/epochtalk_server_web/controllers/poll_test.exs b/test/epochtalk_server_web/controllers/poll_test.exs index 855819b4..4c831742 100644 --- a/test/epochtalk_server_web/controllers/poll_test.exs +++ b/test/epochtalk_server_web/controllers/poll_test.exs @@ -7,11 +7,11 @@ defmodule Test.EpochtalkServerWeb.Controllers.Poll do alias EpochtalkServerWeb.CustomErrors.InvalidPermission setup %{users: %{user: user, admin_user: admin_user, super_admin_user: super_admin_user}} do - board = insert(:board) - admin_board = insert(:board, viewable_by: 1) + board = build(:board) + admin_board = build(:board, viewable_by: 1) # readable by admins but only writeable by super admins - super_admin_board = insert(:board, viewable_by: 1, postable_by: 0) - category = insert(:category) + super_admin_board = build(:board, viewable_by: 1, postable_by: 0) + category = build(:category) build(:board_mapping, attributes: [ diff --git a/test/epochtalk_server_web/controllers/thread_test.exs b/test/epochtalk_server_web/controllers/thread_test.exs index 752c61a8..1686452d 100644 --- a/test/epochtalk_server_web/controllers/thread_test.exs +++ b/test/epochtalk_server_web/controllers/thread_test.exs @@ -3,6 +3,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do import Test.Support.Factory alias EpochtalkServerWeb.CustomErrors.InvalidPermission alias EpochtalkServer.Models.User + alias EpochtalkServer.Models.Board setup %{ users: %{ @@ -11,23 +12,31 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do super_admin_user: super_admin_user } } do - board = insert(:board) - admin_board = insert(:board, viewable_by: 1) - super_admin_board = insert(:board, viewable_by: 1, postable_by: 0) - category = insert(:category) + board = build(:board) + test_move_board = build(:board) + admin_board = build(:board, viewable_by: 1) + super_admin_board = build(:board, viewable_by: 1, postable_by: 0) + category = build(:category) build(:board_mapping, attributes: [ [category: category, view_order: 0], [board: board, category: category, view_order: 1], [board: admin_board, category: category, view_order: 2], - [board: super_admin_board, category: category, view_order: 3] + [board: super_admin_board, category: category, view_order: 3], + [board: test_move_board, category: category, view_order: 4] ] ) factory_threads = build_list(3, :thread, board: board, user: user) + num_thread_replies = 3 + thread_post_count = num_thread_replies + 1 thread = build(:thread, board: board, user: user) + + # create posts in thread created above, this is to get a speciic number of thread replies + build_list(num_thread_replies, :post, user: user, thread: thread.post.thread) + admin_priority_thread = build(:thread, board: board, user: admin_user) admin_board_thread = build(:thread, board: admin_board, user: admin_user) super_admin_board_thread = build(:thread, board: super_admin_board, user: super_admin_user) @@ -35,9 +44,11 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do { :ok, board: board, + test_move_board: test_move_board, admin_board: admin_board, super_admin_board: super_admin_board, factory_threads: factory_threads, + thread_post_count: thread_post_count, thread: thread, admin_board_thread: admin_board_thread, admin_priority_thread: admin_priority_thread, @@ -567,7 +578,8 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do test "after purging thread, decreases thread posters' post count", %{ conn: conn, thread: %{post: %{thread_id: thread_id}}, - users: %{user: %{id: user_id}} + users: %{user: %{id: user_id}}, + thread_post_count: thread_post_count } do {:ok, user} = User.by_id(user_id) old_post_count = user.profile.post_count @@ -579,7 +591,9 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do {:ok, updated_user} = User.by_id(user_id) new_post_count = updated_user.profile.post_count - assert new_post_count == old_post_count - 1 + # we use thread_post_count because all replies to thread are by the + # same user + assert new_post_count == old_post_count - thread_post_count end end @@ -746,4 +760,179 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do assert response["user_id"] == user_id end end + + describe "move/2" do + test "when unauthenticated, returns Unauthorized error", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + test_move_board: %{id: board_id} + } do + response = + conn + |> post(Routes.thread_path(conn, :move, thread_id), %{new_board_id: board_id}) + |> json_response(401) + + assert response["error"] == "Unauthorized" + assert response["message"] == "No resource found" + end + + @tag authenticated: :admin + test "given nonexistant thread, does not move thread", %{ + conn: conn, + test_move_board: %{id: board_id} + } do + response = + conn + |> post(Routes.thread_path(conn, :move, -1), %{new_board_id: board_id}) + |> json_response(400) + + assert response["error"] == "Bad Request" + assert response["message"] == "Error, cannot move thread" + end + + @tag authenticated: :mod + test "when authenticated with insufficient priority, given admin thread, throws forbidden read error", + %{ + conn: conn, + admin_board_thread: %{post: %{thread_id: thread_id}}, + test_move_board: %{id: board_id} + } do + response = + conn + |> post(Routes.thread_path(conn, :move, thread_id), %{new_board_id: board_id}) + |> json_response(403) + + assert response["error"] == "Forbidden" + assert response["message"] == "Unauthorized, you do not have permission to read" + end + + @tag authenticated: :admin + test "when authenticated with insufficient priority, given super admin thread, throws forbidden write error", + %{ + conn: conn, + super_admin_board_thread: %{post: %{thread_id: thread_id}}, + test_move_board: %{id: board_id} + } do + response = + conn + |> post(Routes.thread_path(conn, :move, thread_id), %{new_board_id: board_id}) + |> json_response(403) + + assert response["error"] == "Forbidden" + assert response["message"] == "Unauthorized, you do not have permission to write" + end + + @tag authenticated: :banned + test "when authenticated with banned user, throws InvalidPermission forbidden error", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + test_move_board: %{id: board_id} + } do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + post(conn, Routes.thread_path(conn, :move, thread_id), %{ + new_board_id: board_id + }) + end + end + + @tag authenticated: :mod + test "when authenticated with insufficient priority, given admin created thread, throws forbidden error", + %{ + conn: conn, + admin_priority_thread: %{post: %{thread_id: thread_id}}, + test_move_board: %{id: board_id} + } do + response = + conn + |> post(Routes.thread_path(conn, :move, thread_id), %{new_board_id: board_id}) + |> json_response(403) + + assert response["error"] == "Forbidden" + + assert response["message"] == + "Unauthorized, you do not have permission to move another user's thread" + end + + @tag :authenticated + test "when authenticated, with insufficient permissions, does not move thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + test_move_board: %{id: board_id} + } do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + post(conn, Routes.thread_path(conn, :move, thread_id), %{ + new_board_id: board_id + }) + end + end + + @tag authenticated: :global_mod + test "when authenticated, with valid permissions, moves thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + board: %{id: old_board_id, name: old_board_name}, + test_move_board: %{id: new_board_id} + } do + response = + conn + |> post(Routes.thread_path(conn, :move, thread_id), %{new_board_id: new_board_id}) + |> json_response(200) + + assert response["old_board_id"] == old_board_id + assert response["old_board_name"] == old_board_name + end + + @tag authenticated: :global_mod + test "after moving thread, decreases old board's post count and thread count", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + board: %{id: old_board_id}, + test_move_board: %{id: new_board_id}, + thread_post_count: thread_post_count + } do + {:ok, old_board} = Board.find_by_id(old_board_id) + old_post_count = old_board.post_count + old_thread_count = old_board.thread_count + + conn + |> post(Routes.thread_path(conn, :move, thread_id), %{new_board_id: new_board_id}) + |> json_response(200) + + {:ok, old_board} = Board.find_by_id(old_board_id) + new_post_count = old_board.post_count + new_thread_count = old_board.thread_count + + # assuming one post in thread + assert new_post_count == old_post_count - thread_post_count + assert new_thread_count == old_thread_count - 1 + end + + @tag authenticated: :global_mod + test "after moving thread, increases new board's post count and thread count", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + test_move_board: %{id: new_board_id}, + thread_post_count: thread_post_count + } do + {:ok, new_board} = Board.find_by_id(new_board_id) + old_post_count = new_board.post_count + old_thread_count = new_board.thread_count + + conn + |> post(Routes.thread_path(conn, :move, thread_id), %{new_board_id: new_board_id}) + |> json_response(200) + + {:ok, new_board} = Board.find_by_id(new_board_id) + new_post_count = new_board.post_count + new_thread_count = new_board.thread_count + + # assuming one post in thread + assert new_post_count == old_post_count + thread_post_count + assert new_thread_count == old_thread_count + 1 + end + end end diff --git a/test/support/factories/board.ex b/test/support/factories/board.ex index 3080c826..7eb6d260 100644 --- a/test/support/factories/board.ex +++ b/test/support/factories/board.ex @@ -6,18 +6,30 @@ defmodule Test.Support.Factories.Board do defmacro __using__(_opts) do quote do - def board_factory do - %Board{ - name: sequence(:board_name, &"Board #{&1}"), - description: "description", - slug: sequence(:board_slug, &"board-slug-#{&1}"), + # credo:disable-for-next-line + def board_attributes_factory(attrs) do + %{ + name: Map.get(attrs, :name) || sequence(:board_name, &"Board #{&1}"), + description: Map.get(attrs, :description) || "description", + slug: Map.get(attrs, :slug) || sequence(:board_slug, &"board-slug-#{&1}"), + viewable_by: Map.get(attrs, :viewable_by) || nil, + postable_by: Map.get(attrs, :postable_by) || nil, + right_to_left: Map.get(attrs, :right_to_left) || false, meta: %{ - "disable_self_mod" => false, - "disable_post_edit" => nil, - "disable_signature" => false + "disable_self_mod" => Map.get(attrs, :disable_self_mod) || false, + "disable_post_edit" => Map.get(attrs, :disable_post_edit) || nil, + "disable_signature" => Map.get(attrs, :disable_signature) || false } } end + + def board_factory(attrs \\ %{}) do + attributes = build(:board_attributes, attrs) + + case Board.create(attributes) do + {:ok, board} -> board + end + end end end end diff --git a/test/support/factories/category.ex b/test/support/factories/category.ex index 0ff7410b..af3d13a5 100644 --- a/test/support/factories/category.ex +++ b/test/support/factories/category.ex @@ -6,11 +6,22 @@ defmodule Test.Support.Factories.Category do defmacro __using__(_opts) do quote do - def category_factory do - %Category{ - name: sequence(:category_name, &"Category #{&1}") + def category_attributes_factory(attrs) do + %{ + name: Map.get(attrs, :name) || sequence(:category_name, &"Category #{&1}"), + view_order: Map.get(attrs, :view_order) || nil, + viewable_by: Map.get(attrs, :viewable_by) || nil, + postable_by: Map.get(attrs, :viewable_by) || nil } end + + def category_factory(attrs \\ %{}) do + attributes = build(:category_attributes, attrs) + + case Category.create(attributes) do + {:ok, category} -> category + end + end end end end diff --git a/test/support/factories/post.ex b/test/support/factories/post.ex new file mode 100644 index 00000000..c9e78af9 --- /dev/null +++ b/test/support/factories/post.ex @@ -0,0 +1,27 @@ +defmodule Test.Support.Factories.Post do + @moduledoc """ + Factory for `Post` + """ + alias EpochtalkServer.Models.Post + + defmacro __using__(_opts) do + quote do + def post_attributes_factory(%{user: user, thread: thread} = attrs) do + %{ + "user_id" => user.id, + "thread_id" => thread.id, + "content" => %{ + title: Map.get(attrs, :title) || sequence(:post_title, &"RE: Post title #{&1}"), + body: sequence(:post_body, &"Post body #{&1}") + } + } + end + + def post_factory(%{user: user, thread: thread} = attrs) do + attributes = build(:post_attributes, attrs) + + Post.create(attributes) + end + end + end +end diff --git a/test/support/factories/thread.ex b/test/support/factories/thread.ex index 5cbcf3df..4c1998e7 100644 --- a/test/support/factories/thread.ex +++ b/test/support/factories/thread.ex @@ -20,12 +20,14 @@ defmodule Test.Support.Factories.Thread do } end - def thread_factory(%{user: user} = attrs) do + def thread_factory(%{board: board, user: user} = attrs) do attributes = build(:thread_attributes, attrs) Thread.create(attributes, user) |> case do {:ok, thread} -> + thread_id = thread.post.thread.id + thread_title = thread.post.thread.title thread = thread |> Map.put(:attributes, attributes) if thread.poll == nil, diff --git a/test/support/factory.ex b/test/support/factory.ex index 04de2ccd..0daf6e87 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -12,6 +12,7 @@ defmodule Test.Support.Factory do use Test.Support.Factories.Category use Test.Support.Factories.Board use Test.Support.Factories.Thread + use Test.Support.Factories.Post use Test.Support.Factories.Mention use Test.Support.Factories.Notification use Test.Support.Factories.Poll