diff --git a/lib/epochtalk_server/mailer.ex b/lib/epochtalk_server/mailer.ex index 6eed1491..a9ce23cc 100644 --- a/lib/epochtalk_server/mailer.ex +++ b/lib/epochtalk_server/mailer.ex @@ -79,6 +79,44 @@ defmodule EpochtalkServer.Mailer do |> handle_delivered_email() end + @doc """ + Sends thread purge email + """ + @spec send_thread_purge(email_data :: map) :: {:ok, term} | {:error, term} + def send_thread_purge(%{ + email: email, + title: thread_title, + username: username, + action: action, + mod_username: mod_username + }) do + config = Application.get_env(:epochtalk_server, :frontend_config) + frontend_url = config[:frontend_url] + website_title = config[:website][:title] + from_address = config[:emailer][:options][:from_address] + + content = + generate_from_base_template( + """ +

"#{thread_title}" a thread that you #{action}, has been deleted

+ User "#{mod_username}" has deleted the thread named "#{thread_title}". If you wish to know why this thread was removed please contact a member of the forum moderation team.

+ Visit Forum

+ Raw site URL: #{frontend_url} + """, + config + ) + + new() + |> to({username, email}) + |> from({website_title, from_address}) + |> subject( + "[#{website_title}] \"#{thread_title}\" a thread that you #{action}, has been deleted" + ) + |> html_body(content) + |> deliver() + |> handle_delivered_email() + end + @doc """ Sends mention notification email """ diff --git a/lib/epochtalk_server/models/profile.ex b/lib/epochtalk_server/models/profile.ex index f2007834..363c893e 100644 --- a/lib/epochtalk_server/models/profile.ex +++ b/lib/epochtalk_server/models/profile.ex @@ -62,6 +62,15 @@ defmodule EpochtalkServer.Models.Profile do Repo.update_all(query, inc: [post_count: 1]) end + @doc """ + Decrements the `post_count` field given a `User` id + """ + @spec decrement_post_count(user_id :: non_neg_integer) :: {non_neg_integer(), nil} + def decrement_post_count(user_id) do + query = from p in Profile, where: p.user_id == ^user_id + Repo.update_all(query, inc: [post_count: -1]) + end + @doc """ Creates `Profile` record for a specific `User` """ diff --git a/lib/epochtalk_server/models/thread.ex b/lib/epochtalk_server/models/thread.ex index 7b8d99c2..36661bdd 100644 --- a/lib/epochtalk_server/models/thread.ex +++ b/lib/epochtalk_server/models/thread.ex @@ -10,6 +10,8 @@ defmodule EpochtalkServer.Models.Thread do alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.Poll alias EpochtalkServer.Models.Post + alias EpochtalkServer.Models.Profile + alias EpochtalkServer.Models.Role @moduledoc """ `Thread` model, for performing actions relating to forum threads @@ -24,7 +26,10 @@ defmodule EpochtalkServer.Models.Thread do post_count: non_neg_integer | nil, created_at: NaiveDateTime.t() | nil, imported_at: NaiveDateTime.t() | nil, - updated_at: NaiveDateTime.t() | nil + updated_at: NaiveDateTime.t() | nil, + poster_ids: [non_neg_integer] | nil, + user_id: non_neg_integer | nil, + title: String.t() | nil } @derive {Jason.Encoder, only: [ @@ -50,6 +55,9 @@ defmodule EpochtalkServer.Models.Thread do field :imported_at, :naive_datetime field :updated_at, :naive_datetime has_many :posts, Post + field :poster_ids, {:array, :integer}, virtual: true + field :user_id, :integer, virtual: true + field :title, :string, virtual: true # field :smf_topic, :map, virtual: true end @@ -167,6 +175,92 @@ defmodule EpochtalkServer.Models.Thread do end end + @doc """ + Fully purges a `Thread` from the database + + This sets off a trigger that updates the metadata.boards' thread_count and + post_count accordingly. It also updates the metadata.boards' last post + information. + """ + @spec purge(thread_id :: non_neg_integer) :: + {:ok, thread :: t()} | {:error, Ecto.Changeset.t()} + def purge(thread_id) do + case Repo.transaction(fn -> + # Get all poster's user id's from thread and decrement post count + # decrement each poster's post count by how many post they have in the + # current thread + poster_ids_query = + from p in Post, + where: p.thread_id == ^thread_id, + select: p.user_id + + poster_ids = Repo.all(poster_ids_query) + + # Update user profile post count for each post + Enum.each(poster_ids, &Profile.decrement_post_count(&1)) + + # Get title and user_id of first post in thread + query_first_thread_post_data = + from p in Post, + left_join: t in Thread, + on: t.id == p.thread_id, + left_join: b in Board, + on: b.id == t.board_id, + where: p.thread_id == ^thread_id, + order_by: [p.created_at], + limit: 1, + select: %{ + title: p.content["title"], + user_id: p.user_id, + board_name: b.name + } + + # get unique poster ids to send emails + unique_poster_ids = Enum.uniq(poster_ids) + + # query thread before purging + purged_thread = + Repo.one!(query_first_thread_post_data) + |> Map.put(:poster_ids, unique_poster_ids) + + # remove thread + delete_query = + from t in Thread, + where: t.id == ^thread_id + + Repo.delete_all(delete_query) + + # return data for purged thread + purged_thread + 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 + """ + @spec set_sticky(id :: integer, sticky :: boolean) :: {non_neg_integer, nil | [term()]} + def set_sticky(id, sticky) when is_integer(id) and is_boolean(sticky) do + query = from t in Thread, where: t.id == ^id + Repo.update_all(query, set: [sticky: sticky]) + end + + @doc """ + Sets boolean indicating if the specified `Thread` is locked given a `Thread` id + """ + @spec set_locked(id :: integer, locked :: boolean) :: {non_neg_integer, nil | [term()]} + def set_locked(id, locked) when is_integer(id) and is_boolean(locked) do + query = from t in Thread, where: t.id == ^id + Repo.update_all(query, set: [locked: locked]) + end + @doc """ Returns boolean indicating if `Thread` is locked or nil if it does not exist """ @@ -425,7 +519,14 @@ defmodule EpochtalkServer.Models.Thread do } first_post = Repo.one(query_first_thread_post_data) - Map.put(first_post, :user, Repo.preload(first_post.user, :roles)) + + # any time we preload a users roles, we need to handle empty and banned roles + preloaded_user = + Repo.preload(first_post.user, :roles) + |> Role.handle_empty_user_roles() + |> Role.handle_banned_user_role() + + Map.put(first_post, :user, preloaded_user) end @doc """ diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 8d90d4d3..5c11d3fe 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -196,6 +196,20 @@ defmodule EpochtalkServer.Models.User do Repo.one(query) end + @doc """ + Gets a `User` email from the database by `id` list + """ + @spec email_by_id_list(id :: [integer]) :: + [user_data :: map()] | {:error, :user_not_found} + def email_by_id_list([h | _] = id_list) when is_list(id_list) and is_integer(h) do + query = + from u in User, + where: u.id in ^id_list, + select: %{email: u.email, user_id: u.id, username: u.username} + + Repo.all(query) + end + @doc """ Gets a `User` username from the database by `id` """ @@ -327,7 +341,7 @@ defmodule EpochtalkServer.Models.User do {:ok, user :: t()} | {:error, :ban_error} def handle_malicious_user(%User{} = user, ip) do # convert ip tuple into string - ip_str = ip |> :inet_parse.ntoa() |> to_string + ip_str = ip |> :inet_parse.ntoa() |> to_string |> String.replace("::ffff:", "") # calculate user's malicious score from ip, nil if less than 1 malicious_score = BannedAddress.calculate_malicious_score_from_ip(ip_str) # set user's malicious score diff --git a/lib/epochtalk_server/models/watch_thread.ex b/lib/epochtalk_server/models/watch_thread.ex index 1f1274a6..78c6f944 100644 --- a/lib/epochtalk_server/models/watch_thread.ex +++ b/lib/epochtalk_server/models/watch_thread.ex @@ -54,7 +54,7 @@ defmodule EpochtalkServer.Models.WatchThread do case Repo.insert(watch_thread_cs) do {:ok, db_watch_thread} -> - db_watch_thread + {:ok, db_watch_thread} {:error, %Ecto.Changeset{ @@ -75,7 +75,7 @@ defmodule EpochtalkServer.Models.WatchThread do """ @spec delete(user :: User.t(), thread_id :: non_neg_integer) :: {non_neg_integer(), nil | [term()]} - def delete(%User{} = user, thread_id) do + def delete(%{} = user, thread_id) do query = from w in WatchThread, where: w.user_id == ^user.id and w.thread_id == ^thread_id diff --git a/lib/epochtalk_server_web/controllers/thread.ex b/lib/epochtalk_server_web/controllers/thread.ex index 39a45e46..112e79d2 100644 --- a/lib/epochtalk_server_web/controllers/thread.ex +++ b/lib/epochtalk_server_web/controllers/thread.ex @@ -5,6 +5,7 @@ defmodule EpochtalkServerWeb.Controllers.Thread do Controller For `Thread` related API requests """ alias EpochtalkServer.Auth.Guardian + alias EpochtalkServer.Mailer alias EpochtalkServerWeb.ErrorHelpers alias EpochtalkServerWeb.Helpers.Validate alias EpochtalkServerWeb.Helpers.ACL @@ -18,6 +19,8 @@ defmodule EpochtalkServerWeb.Controllers.Thread do alias EpochtalkServer.Models.BoardModerator alias EpochtalkServer.Models.UserThreadView alias EpochtalkServer.Models.MetadataThread + alias EpochtalkServer.Models.ModerationLog + alias EpochtalkServer.Models.WatchThread alias EpochtalkServer.Models.WatchBoard alias EpochtalkServer.Models.AutoModeration alias EpochtalkServer.Models.UserActivity @@ -220,6 +223,295 @@ defmodule EpochtalkServerWeb.Controllers.Thread do end end + @doc """ + Used to watch `Thread` + """ + def watch(conn, attrs) do + with user <- Guardian.Plug.current_resource(conn), + thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true), + :ok <- ACL.allow!(conn, "watchlist.watchThread"), + user_priority <- ACL.get_user_priority(conn), + {:can_read, {:ok, true}} <- + {:can_read, Board.get_read_access_by_thread_id(thread_id, user_priority)}, + {:is_active, true} <- + {:is_active, User.is_active?(user.id)}, + {:ok, watch_thread} <- WatchThread.create(user, thread_id) do + render(conn, :watch, thread: watch_thread) + else + {:can_read, {:ok, false}} -> + ErrorHelpers.render_json_error( + conn, + 403, + "Unauthorized, you do not have permission to read" + ) + + {:is_active, false} -> + ErrorHelpers.render_json_error( + conn, + 400, + "Account must be active to watch thread" + ) + + {:error, data} -> + ErrorHelpers.render_json_error(conn, 400, data) + + _ -> + ErrorHelpers.render_json_error(conn, 400, "Error, cannot watch thread") + end + end + + @doc """ + Used to unwatch `Thread` + """ + def unwatch(conn, attrs) do + with user <- Guardian.Plug.current_resource(conn), + thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true), + :ok <- ACL.allow!(conn, "watchlist.unwatchThread"), + user_priority <- ACL.get_user_priority(conn), + {:can_read, {:ok, true}} <- + {:can_read, Board.get_read_access_by_thread_id(thread_id, user_priority)}, + {:is_active, true} <- + {:is_active, User.is_active?(user.id)}, + {1, nil} <- WatchThread.delete(user, thread_id) do + render(conn, :watch, thread: %{thread_id: thread_id, user_id: user.id}) + else + {:can_read, {:ok, false}} -> + ErrorHelpers.render_json_error( + conn, + 403, + "Unauthorized, you do not have permission to read" + ) + + {:is_active, false} -> + ErrorHelpers.render_json_error( + conn, + 400, + "Account must be active to unwatch thread" + ) + + {:error, data} -> + ErrorHelpers.render_json_error(conn, 400, data) + + _ -> + ErrorHelpers.render_json_error(conn, 400, "Error, cannot unwatch thread") + end + end + + @doc """ + Used to lock `Thread` + """ + def lock(conn, attrs) do + with user <- Guardian.Plug.current_resource(conn), + thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true), + locked <- Validate.cast(attrs, "locked", :boolean, required: true), + :ok <- ACL.allow!(conn, "threads.lock"), + 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_lock(user, thread_id)}, + {1, nil} <- Thread.set_locked(thread_id, locked) do + render(conn, :lock, thread: %{thread_id: thread_id, locked: locked}) + 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 modify the lock on 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 modify lock on thread" + ) + + {:error, data} -> + ErrorHelpers.render_json_error(conn, 400, data) + + _ -> + ErrorHelpers.render_json_error(conn, 400, "Error, cannot lock thread") + end + end + + @doc """ + Used to sticky `Thread` + """ + def sticky(conn, attrs) do + with user <- Guardian.Plug.current_resource(conn), + thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true), + sticky <- Validate.cast(attrs, "sticky", :boolean, required: true), + :ok <- ACL.allow!(conn, "threads.sticky"), + 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_sticky(user, thread_id)}, + {1, nil} <- Thread.set_sticky(thread_id, sticky) do + render(conn, :sticky, thread: %{thread_id: thread_id, sticky: sticky}) + 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 modify 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 modify sticky on thread" + ) + + {:error, data} -> + ErrorHelpers.render_json_error(conn, 400, data) + + _ -> + ErrorHelpers.render_json_error(conn, 400, "Error, cannot sticky thread") + end + end + + @doc """ + Used to purge `Thread` + """ + def purge(conn, attrs) do + with user <- Guardian.Plug.current_resource(conn), + thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true), + :ok <- ACL.allow!(conn, "threads.purge"), + 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_purge(user, thread_id)}, + {:ok, thread} <- Thread.purge(thread_id) do + poster_data = User.email_by_id_list(thread.poster_ids) + + # Email thread owner and subscribers + Enum.each(poster_data, fn %{user_id: user_id, email: email, username: username} -> + action = if thread.user_id == user_id, do: "created", else: "participated in" + + Mailer.send_thread_purge(%{ + email: email, + title: thread.title, + username: username, + action: action, + mod_username: user.username + }) + end) + + # parse moderator's ip, remove ipv6 prefix if present + mod_ip_str = + conn.remote_ip + |> :inet_parse.ntoa() + |> to_string + |> String.replace("::ffff:", "") + + {:ok, _moderation_log} = + ModerationLog.create(%{ + mod: %{username: user.username, id: user.id, ip: mod_ip_str}, + action: %{ + api_url: "/api/threads/#{thread_id}", + api_method: "delete", + type: "threads.purge", + obj: thread + } + }) + + render(conn, :purge, thread: thread) + 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 modify 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 modify purge on thread" + ) + + {:error, data} -> + ErrorHelpers.render_json_error(conn, 400, data) + + _ -> + ErrorHelpers.render_json_error(conn, 400, "Error, cannot purge thread") + end + end + @doc """ Used to convert `Thread` slug to id """ @@ -317,7 +609,12 @@ defmodule EpochtalkServerWeb.Controllers.Thread do defp check_view_ip(conn, thread_id) do # convert ip tuple into string - viewer_ip = conn.remote_ip |> :inet_parse.ntoa() |> to_string + viewer_ip = + conn.remote_ip + |> :inet_parse.ntoa() + |> to_string + |> String.replace("::ffff:", "") + viewer_ip_key = viewer_ip <> Integer.to_string(thread_id) handle_cooloff(viewer_ip_key, viewer_ip, thread_id, true) end @@ -346,4 +643,46 @@ defmodule EpochtalkServerWeb.Controllers.Thread do do: :ok == ACL.allow!(user, "threads.moderated"), else: true end + + defp can_authed_user_bypass_owner_on_thread_purge(user, thread_id) do + post = Thread.get_first_post_data_by_id(thread_id) + + ACL.bypass_post_owner( + user, + post, + "threads.purge", + "owner", + false, + true, + true + ) + end + + defp can_authed_user_bypass_owner_on_thread_sticky(user, thread_id) do + post = Thread.get_first_post_data_by_id(thread_id) + + ACL.bypass_post_owner( + user, + post, + "threads.sticky", + "owner", + false, + true, + true + ) + end + + defp can_authed_user_bypass_owner_on_thread_lock(user, thread_id) do + post = Thread.get_first_post_data_by_id(thread_id) + + ACL.bypass_post_owner( + user, + post, + "threads.lock", + "owner", + post.user_id == user.id, + true, + true + ) + end end diff --git a/lib/epochtalk_server_web/helpers/moderation_log_helper.ex b/lib/epochtalk_server_web/helpers/moderation_log_helper.ex index 1af92517..cf7f3f10 100644 --- a/lib/epochtalk_server_web/helpers/moderation_log_helper.ex +++ b/lib/epochtalk_server_web/helpers/moderation_log_helper.ex @@ -584,7 +584,7 @@ defmodule EpochtalkServerWeb.Helpers.ModerationLogHelper do def get_display_data(action_type) when action_type == "threads.purge" do %{ get_display_text: fn data -> - "purged thread '#{data.title}' created by user '#{data.author.username}' from board '#{data.old_board_name}'" + "purged thread '#{data.title}' created by user '#{data.author.username}' from board '#{data.board_name}'" end, get_display_url: fn _ -> nil end, data_query: fn data -> diff --git a/lib/epochtalk_server_web/json/thread_json.ex b/lib/epochtalk_server_web/json/thread_json.ex index 26891063..b277f244 100644 --- a/lib/epochtalk_server_web/json/thread_json.ex +++ b/lib/epochtalk_server_web/json/thread_json.ex @@ -89,6 +89,57 @@ defmodule EpochtalkServerWeb.Controllers.ThreadJSON do if board_banned, do: Map.put(result, :board_banned, board_banned), else: result end + @doc """ + Renders sticky `Thread`. + + iex> thread = %{ + iex> thread_id: 2, + iex> sticky: true + iex> } + iex> EpochtalkServerWeb.Controllers.ThreadJSON.sticky(%{thread: thread}) + thread + """ + def sticky(%{thread: %{thread_id: thread_id, sticky: sticky}}), + do: %{thread_id: thread_id, sticky: sticky} + + @doc """ + Renders locked `Thread`. + + iex> thread = %{ + iex> thread_id: 2, + iex> locked: false + iex> } + iex> EpochtalkServerWeb.Controllers.ThreadJSON.lock(%{thread: thread}) + thread + """ + def lock(%{thread: %{thread_id: thread_id, locked: locked}}), + do: %{thread_id: thread_id, locked: locked} + + @doc """ + Renders watched `Thread`. + + iex> thread = %{ + iex> thread_id: 2, + iex> user_id: 1 + iex> } + iex> EpochtalkServerWeb.Controllers.ThreadJSON.watch(%{thread: thread}) + thread + """ + def watch(%{thread: %{thread_id: thread_id, user_id: user_id}}), + do: %{thread_id: thread_id, user_id: user_id} + + @doc """ + Renders purge `Thread`. + + iex> thread = %{ + iex> thread_id: 2 + iex> } + iex> EpochtalkServerWeb.Controllers.ThreadJSON.purge(%{thread: thread}) + thread + """ + def purge(%{thread: thread}), + do: thread + @doc """ Renders `Thread` id for slug to id route. """ diff --git a/lib/epochtalk_server_web/plugs/prepare_parse.ex b/lib/epochtalk_server_web/plugs/prepare_parse.ex index 7c771d6e..ea192c4e 100644 --- a/lib/epochtalk_server_web/plugs/prepare_parse.ex +++ b/lib/epochtalk_server_web/plugs/prepare_parse.ex @@ -34,9 +34,15 @@ defmodule EpochtalkServerWeb.Plugs.PrepareParse do end defp try_decode(conn, body) do - case Jason.decode(body) do - {:ok, _result} -> update_in(conn.assigns[:raw_body], &[body | &1 || []]) - {:error, _reason} -> raise MalformedPayload + %{method: method} = conn + + if method in @methods and @env != :test do + case Jason.decode(body) do + {:ok, _result} -> update_in(conn.assigns[:raw_body], &[body | &1 || []]) + {:error, _reason} -> raise MalformedPayload + end + else + conn end end end diff --git a/lib/epochtalk_server_web/plugs/track_ip.ex b/lib/epochtalk_server_web/plugs/track_ip.ex index 81e24b5e..fd079c34 100644 --- a/lib/epochtalk_server_web/plugs/track_ip.ex +++ b/lib/epochtalk_server_web/plugs/track_ip.ex @@ -26,7 +26,13 @@ defmodule EpochtalkServerWeb.Plugs.TrackIp do defp maybe_save_user_ip_to_database(conn) do register_before_send(conn, fn conn -> user = Guardian.Plug.current_resource(conn) - user_ip = conn.remote_ip |> :inet_parse.ntoa() |> to_string + + user_ip = + conn.remote_ip + |> :inet_parse.ntoa() + |> to_string + |> String.replace("::ffff:", "") + UserIp.maybe_track(user, user_ip) conn end) diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index ad5e0c1e..f0641d97 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -46,11 +46,16 @@ defmodule EpochtalkServerWeb.Router do get "/admin/roles/all", Role, :all put "/admin/roles/update", Role, :update post "/threads", Thread, :create + post "/threads/:thread_id/lock", Thread, :lock + post "/threads/:thread_id/sticky", Thread, :sticky + delete "/threads/:thread_id", Thread, :purge post "/threads/:thread_id/polls/vote", Poll, :vote delete "/threads/:thread_id/polls/vote", Poll, :delete_vote post "/threads/:thread_id/polls/lock", Poll, :lock put "/threads/:thread_id/polls", Poll, :update post "/threads/:thread_id/polls", Poll, :create + post "/watchlist/threads/:thread_id", Thread, :watch + delete "/watchlist/threads/:thread_id", Thread, :unwatch get "/posts/draft", PostDraft, :by_user_id put "/posts/draft", PostDraft, :upsert post "/posts", Post, :create diff --git a/test/epochtalk_server_web/controllers/moderation_log_test.exs b/test/epochtalk_server_web/controllers/moderation_log_test.exs index 9c7c9165..6206f48d 100644 --- a/test/epochtalk_server_web/controllers/moderation_log_test.exs +++ b/test/epochtalk_server_web/controllers/moderation_log_test.exs @@ -1072,7 +1072,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.ModerationLog do obj: %{ title: thread_title, user_id: user.id, - old_board_name: board.name + board_name: board.name } }) diff --git a/test/epochtalk_server_web/controllers/thread_test.exs b/test/epochtalk_server_web/controllers/thread_test.exs index 5c80bd4c..752c61a8 100644 --- a/test/epochtalk_server_web/controllers/thread_test.exs +++ b/test/epochtalk_server_web/controllers/thread_test.exs @@ -1,11 +1,19 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do use Test.Support.ConnCase, async: true import Test.Support.Factory + alias EpochtalkServerWeb.CustomErrors.InvalidPermission + alias EpochtalkServer.Models.User - setup %{users: %{user: user, admin_user: admin_user, super_admin_user: super_admin_user}} do + 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) - super_admin_board = insert(:board, viewable_by: 0) + super_admin_board = insert(:board, viewable_by: 1, postable_by: 0) category = insert(:category) build(:board_mapping, @@ -17,18 +25,23 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do ] ) - threads = build_list(3, :thread, board: board, user: user) - admin_threads = build_list(3, :thread, board: admin_board, user: admin_user) - super_admin_threads = build_list(3, :thread, board: super_admin_board, user: super_admin_user) + factory_threads = build_list(3, :thread, board: board, user: user) + + thread = build(:thread, board: board, user: user) + 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) { :ok, board: board, admin_board: admin_board, super_admin_board: super_admin_board, - threads: threads, - admin_threads: admin_threads, - super_admin_threads: super_admin_threads + factory_threads: factory_threads, + thread: thread, + admin_board_thread: admin_board_thread, + admin_priority_thread: admin_priority_thread, + super_admin_board_thread: super_admin_board_thread } end @@ -46,7 +59,7 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do test "given an id for existing board, gets threads", %{ conn: conn, board: board, - threads: factory_threads + factory_threads: factory_threads } do response = conn @@ -181,4 +194,556 @@ defmodule Test.EpochtalkServerWeb.Controllers.Thread do assert Map.has_key?(response, "sticky") == true end end + + describe "lock/2" do + test "when unauthenticated, returns Unauthorized error", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :lock, thread_id), %{"locked" => true}) + |> json_response(401) + + assert response["error"] == "Unauthorized" + assert response["message"] == "No resource found" + end + + @tag authenticated: :admin + test "given nonexistant thread, does not lock thread", %{ + conn: conn + } do + response = + conn + |> post(Routes.thread_path(conn, :lock, -1), %{"locked" => true}) + |> json_response(400) + + assert response["error"] == "Bad Request" + assert response["message"] == "Error, cannot lock thread" + end + + @tag authenticated: :mod + test "when authenticated with insufficient permissions, throws forbidden read error", %{ + conn: conn, + admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :lock, thread_id), %{"locked" => true}) + |> 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 permissions, throws forbidden write error", %{ + conn: conn, + super_admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :lock, thread_id), %{"locked" => true}) + |> 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}} + } do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + post(conn, Routes.thread_path(conn, :lock, thread_id), %{"locked" => true}) + end + end + + @tag authenticated: :global_mod + test "when authenticated with insufficient priority, throws forbidden error", %{ + conn: conn, + admin_priority_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :lock, thread_id), %{"locked" => true}) + |> json_response(403) + + assert response["error"] == "Forbidden" + + assert response["message"] == + "Unauthorized, you do not have permission to modify the lock on another user's thread" + end + + @tag :authenticated + test "given thread and user who does not own the thread, does not lock thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + post(conn, Routes.thread_path(conn, :lock, thread_id), %{"locked" => true}) + end + end + + @tag authenticated: :global_mod + test "given thread that authenticated user moderates, locks thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :lock, thread_id), %{"locked" => true}) + |> json_response(200) + + assert response["locked"] == true + assert response["thread_id"] == thread_id + end + + @tag authenticated: :global_mod + test "given thread that authenticated user moderates, unlocks thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + conn + |> post(Routes.thread_path(conn, :lock, thread_id), %{"locked" => true}) + |> json_response(200) + + response = + conn + |> post(Routes.thread_path(conn, :lock, thread_id), %{"locked" => false}) + |> json_response(200) + + assert response["locked"] == false + assert response["thread_id"] == thread_id + end + end + + describe "sticky/2" do + test "when unauthenticated, returns Unauthorized error", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => true}) + |> json_response(401) + + assert response["error"] == "Unauthorized" + assert response["message"] == "No resource found" + end + + @tag authenticated: :admin + test "given nonexistant thread, does not sticky thread", %{ + conn: conn + } do + response = + conn + |> post(Routes.thread_path(conn, :sticky, -1), %{"sticky" => true}) + |> json_response(400) + + assert response["error"] == "Bad Request" + assert response["message"] == "Error, cannot sticky thread" + end + + @tag authenticated: :mod + test "when authenticated with insufficient permissions, throws forbidden read error", %{ + conn: conn, + admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => true}) + |> 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 permissions, throws forbidden write error", %{ + conn: conn, + super_admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => true}) + |> 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}} + } do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + post(conn, Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => true}) + end + end + + @tag authenticated: :global_mod + test "when authenticated with insufficient priority, throws forbidden error", %{ + conn: conn, + admin_priority_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => true}) + |> json_response(403) + + assert response["error"] == "Forbidden" + + assert response["message"] == + "Unauthorized, you do not have permission to modify another user's thread" + end + + @tag :authenticated + test "given thread and user who does not own the thread, does not sticky thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + post(conn, Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => true}) + end + end + + @tag authenticated: :global_mod + test "given thread that authenticated user moderates, stickies thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => true}) + |> json_response(200) + + assert response["sticky"] == true + assert response["thread_id"] == thread_id + end + + @tag authenticated: :global_mod + test "given thread that authenticated user moderates, unstickies thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + conn + |> post(Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => true}) + |> json_response(200) + + response = + conn + |> post(Routes.thread_path(conn, :sticky, thread_id), %{"sticky" => false}) + |> json_response(200) + + assert response["sticky"] == false + assert response["thread_id"] == thread_id + end + end + + describe "purge/2" do + test "when unauthenticated, returns Unauthorized error", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :purge, thread_id), %{}) + |> json_response(401) + + assert response["error"] == "Unauthorized" + assert response["message"] == "No resource found" + end + + @tag authenticated: :admin + test "given nonexistant thread, does not purge thread", %{ + conn: conn + } do + response = + conn + |> delete(Routes.thread_path(conn, :purge, -1), %{}) + |> json_response(400) + + assert response["error"] == "Bad Request" + assert response["message"] == "Error, cannot purge thread" + end + + @tag authenticated: :mod + test "when authenticated with insufficient permissions, throws forbidden read error", %{ + conn: conn, + admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :purge, thread_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 permissions, throws forbidden write error", %{ + conn: conn, + super_admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :purge, thread_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}} + } do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + delete(conn, Routes.thread_path(conn, :purge, thread_id), %{}) + end + end + + @tag authenticated: :global_mod + test "when authenticated with insufficient priority, throws forbidden error", %{ + conn: conn, + admin_priority_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :purge, thread_id), %{}) + |> json_response(403) + + assert response["error"] == "Forbidden" + + assert response["message"] == + "Unauthorized, you do not have permission to modify another user's thread" + end + + @tag :authenticated + test "given thread and user who does not own the thread, does not purge thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + assert_raise InvalidPermission, + ~r/^Forbidden, invalid permissions to perform this action/, + fn -> + delete(conn, Routes.thread_path(conn, :purge, thread_id), %{}) + end + end + + @tag authenticated: :global_mod + test "given thread that authenticated user moderates, purges thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + users: %{user: %{id: user_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :purge, thread_id), %{}) + |> json_response(200) + + assert String.starts_with?(response["board_name"], "Board") + assert String.starts_with?(response["title"], "Thread title") + assert response["poster_ids"] == [user_id] + assert response["user_id"] == user_id + end + + @tag authenticated: :global_mod + test "after purging thread, decreases thread posters' post count", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + users: %{user: %{id: user_id}} + } do + {:ok, user} = User.by_id(user_id) + old_post_count = user.profile.post_count + + conn + |> delete(Routes.thread_path(conn, :purge, thread_id), %{}) + |> json_response(200) + + {:ok, updated_user} = User.by_id(user_id) + new_post_count = updated_user.profile.post_count + + assert new_post_count == old_post_count - 1 + end + end + + describe "watch/2" do + test "when unauthenticated, returns Unauthorized error", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :watch, thread_id), %{}) + |> json_response(401) + + assert response["error"] == "Unauthorized" + assert response["message"] == "No resource found" + end + + @tag authenticated: :admin + test "given nonexistant thread, does not watch thread", %{ + conn: conn + } do + response = + conn + |> post(Routes.thread_path(conn, :watch, -1), %{}) + |> json_response(400) + + assert response["error"] == "Bad Request" + assert response["message"] == "Error, cannot watch thread" + end + + @tag authenticated: :mod + test "when authenticated with insufficient permissions, throws forbidden read error", %{ + conn: conn, + admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :watch, thread_id), %{}) + |> json_response(403) + + assert response["error"] == "Forbidden" + assert response["message"] == "Unauthorized, you do not have permission to read" + end + + @tag authenticated: :global_mod + test "when authenticated with insufficient permissions, throws forbidden error", %{ + conn: conn, + super_admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :watch, thread_id), %{}) + |> json_response(403) + + assert response["error"] == "Forbidden" + + assert response["message"] == + "Unauthorized, you do not have permission to read" + end + + @tag authenticated: :global_mod + test "given a valid thread, watches", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + users: %{global_mod_user: %{id: user_id}} + } do + response = + conn + |> post(Routes.thread_path(conn, :watch, thread_id), %{}) + |> json_response(200) + + assert response["thread_id"] == thread_id + assert response["user_id"] == user_id + end + end + + describe "unwatch/2" do + test "when unauthenticated, returns Unauthorized error", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :unwatch, thread_id), %{}) + |> json_response(401) + + assert response["error"] == "Unauthorized" + assert response["message"] == "No resource found" + end + + @tag authenticated: :admin + test "given nonexistant thread, does not unwatch thread", %{ + conn: conn + } do + response = + conn + |> delete(Routes.thread_path(conn, :unwatch, -1), %{}) + |> json_response(400) + + assert response["error"] == "Bad Request" + assert response["message"] == "Error, cannot unwatch thread" + end + + @tag authenticated: :mod + test "when authenticated with insufficient permissions, throws forbidden read error", %{ + conn: conn, + admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :unwatch, thread_id), %{}) + |> json_response(403) + + assert response["error"] == "Forbidden" + assert response["message"] == "Unauthorized, you do not have permission to read" + end + + @tag authenticated: :global_mod + test "when authenticated with insufficient permissions, throws forbidden error", %{ + conn: conn, + super_admin_board_thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :unwatch, thread_id), %{}) + |> json_response(403) + + assert response["error"] == "Forbidden" + + assert response["message"] == + "Unauthorized, you do not have permission to read" + end + + @tag authenticated: :global_mod + test "given a valid thread that is not watched, does not unwatch thread", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}} + } do + response = + conn + |> delete(Routes.thread_path(conn, :unwatch, thread_id), %{}) + |> json_response(400) + + assert response["error"] == "Bad Request" + assert response["message"] == "Error, cannot unwatch thread" + end + + @tag authenticated: :global_mod + test "given a valid thread, unwatches", %{ + conn: conn, + thread: %{post: %{thread_id: thread_id}}, + users: %{global_mod_user: %{id: user_id}} + } do + conn + |> post(Routes.thread_path(conn, :watch, thread_id), %{}) + |> json_response(200) + + response = + conn + |> delete(Routes.thread_path(conn, :unwatch, thread_id), %{}) + |> json_response(200) + + assert response["thread_id"] == thread_id + assert response["user_id"] == user_id + end + end end diff --git a/test/epochtalk_server_web/json/thread_json_test.exs b/test/epochtalk_server_web/json/thread_json_test.exs new file mode 100644 index 00000000..93ae622d --- /dev/null +++ b/test/epochtalk_server_web/json/thread_json_test.exs @@ -0,0 +1,7 @@ +defmodule Test.EpochtalkServerWeb.Controllers.ThreadJSON do + use Test.Support.ConnCase, async: true + alias EpochtalkServerWeb.Controllers.ThreadJSON + + # Specify that we want to use doctests: + doctest ThreadJSON +end diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 9695629e..f7a6e2b3 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -14,17 +14,47 @@ defmodule Test.Support.DataCase do this option is not recommended for other databases. """ - # no_login username/email/password from user seed in `mix test` (see mix.exs) - @test_no_login_username "no_login" - @test_no_login_email "no_login@test.com" - @test_no_login_password "password" - @test_no_login_user_attrs %{ - username: @test_no_login_username, - email: @test_no_login_email, - password: @test_no_login_password + # super admin (1) username/email/password from user seed in `mix test` (see mix.exs) + @test_super_admin_username "superadmin" + @test_super_admin_email "superadmin@test.com" + @test_super_admin_password "password" + @test_super_admin_user_attrs %{ + username: @test_super_admin_username, + email: @test_super_admin_email, + password: @test_super_admin_password + } + + # admin (2) username/email/password from user seed in `mix test` (see mix.exs) + @test_admin_username "admin" + @test_admin_email "admin@test.com" + @test_admin_password "password" + @test_admin_user_attrs %{ + username: @test_admin_username, + email: @test_admin_email, + password: @test_admin_password + } + + # global mod (3) username/email/password from user seed in `mix test` (see mix.exs) + @test_global_mod_username "globalmod" + @test_global_mod_email "globalmod@test.com" + @test_global_mod_password "password" + @test_global_mod_user_attrs %{ + username: @test_global_mod_username, + email: @test_global_mod_email, + password: @test_global_mod_password } - # username/email/password from user seed in `mix test` (see mix.exs) + # mod (4) username/email/password from user seed in `mix test` (see mix.exs) + @test_mod_username "mod" + @test_mod_email "mod@test.com" + @test_mod_password "password" + @test_mod_user_attrs %{ + username: @test_mod_username, + email: @test_mod_email, + password: @test_mod_password + } + + # user (5) username/email/password from user seed in `mix test` (see mix.exs) @test_username "user" @test_email "user@test.com" @test_password "password" @@ -34,24 +64,64 @@ defmodule Test.Support.DataCase do password: @test_password } - # admin username/email/password from user seed in `mix test` (see mix.exs) - @test_admin_username "admin" - @test_admin_email "admin@test.com" - @test_admin_password "password" - @test_admin_user_attrs %{ - username: @test_admin_username, - email: @test_admin_email, - password: @test_admin_password + # patroller (6) username/email/password from user seed in `mix test` (see mix.exs) + @test_patroller_username "patroller" + @test_patroller_email "patroller@test.com" + @test_patroller_password "password" + @test_patroller_user_attrs %{ + username: @test_patroller_username, + email: @test_patroller_email, + password: @test_patroller_password } - # super admin username/email/password from user seed in `mix test` (see mix.exs) - @test_super_admin_username "superadmin" - @test_super_admin_email "superadmin@test.com" - @test_super_admin_password "password" - @test_super_admin_user_attrs %{ - username: @test_super_admin_username, - email: @test_super_admin_email, - password: @test_super_admin_password + # newbie (7) username/email/password from user seed in `mix test` (see mix.exs) + @test_newbie_username "newbie" + @test_newbie_email "newbie@test.com" + @test_newbie_password "password" + @test_newbie_user_attrs %{ + username: @test_newbie_username, + email: @test_newbie_email, + password: @test_newbie_password + } + + # banned (8) username/email/password from user seed in `mix test` (see mix.exs) + @test_banned_username "banned" + @test_banned_email "banned@test.com" + @test_banned_password "password" + @test_banned_user_attrs %{ + username: @test_banned_username, + email: @test_banned_email, + password: @test_banned_password + } + + # anonymous (9) username/email/password from user seed in `mix test` (see mix.exs) + @test_anonymous_username "anonymous" + @test_anonymous_email "anonymous@test.com" + @test_anonymous_password "password" + @test_anonymous_user_attrs %{ + username: @test_anonymous_username, + email: @test_anonymous_email, + password: @test_anonymous_password + } + + # private (10) username/email/password from user seed in `mix test` (see mix.exs) + @test_private_username "private" + @test_private_email "private@test.com" + @test_private_password "password" + @test_private_user_attrs %{ + username: @test_private_username, + email: @test_private_email, + password: @test_private_password + } + + # no_login username/email/password from user seed in `mix test` (see mix.exs) + @test_no_login_username "no_login" + @test_no_login_email "no_login@test.com" + @test_no_login_password "password" + @test_no_login_user_attrs %{ + username: @test_no_login_username, + email: @test_no_login_email, + password: @test_no_login_password } use ExUnit.CaseTemplate @@ -70,25 +140,46 @@ defmodule Test.Support.DataCase do setup tags do alias EpochtalkServer.Models.User Test.Support.DataCase.setup_sandbox(tags) - {:ok, no_login_user} = User.by_username(@test_no_login_username) - {:ok, user} = User.by_username(@test_username) - {:ok, admin_user} = User.by_username(@test_admin_username) {:ok, super_admin_user} = User.by_username(@test_super_admin_username) + {:ok, admin_user} = User.by_username(@test_admin_username) + {:ok, global_mod_user} = User.by_username(@test_global_mod_username) + {:ok, mod_user} = User.by_username(@test_mod_username) + {:ok, user} = User.by_username(@test_username) + {:ok, patroller_user} = User.by_username(@test_patroller_username) + {:ok, newbie_user} = User.by_username(@test_newbie_username) + {:ok, banned_user} = User.by_username(@test_banned_username) + {:ok, anonymous_user} = User.by_username(@test_anonymous_username) + {:ok, private_user} = User.by_username(@test_private_username) + {:ok, no_login_user} = User.by_username(@test_no_login_username) { :ok, [ users: %{ - no_login_user: no_login_user, - user: user, + super_admin_user: super_admin_user, admin_user: admin_user, - super_admin_user: super_admin_user + global_mod_user: global_mod_user, + mod_user: mod_user, + user: user, + patroller_user: patroller_user, + newbie_user: newbie_user, + banned_user: banned_user, + anonymous_user: anonymous_user, + private_user: private_user, + no_login_user: no_login_user }, user_attrs: %{ - no_login_user: @test_no_login_user_attrs, - user: @test_user_attrs, + super_admin_user: @test_super_admin_user_attrs, admin_user: @test_admin_user_attrs, - super_admin_user: @test_super_admin_user_attrs + global_mod_user: @test_global_mod_user_attrs, + mod_user: @test_mod_user_attrs, + user: @test_user_attrs, + patroller_user: @test_patroller_user_attrs, + newbie_user: @test_newbie_user_attrs, + banned_user: @test_banned_user_attrs, + anonymous_user: @test_anonymous_user_attrs, + private_user: @test_private_user_attrs, + no_login_user: @test_no_login_user_attrs } ] }