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
}
]
}