api: ensure only visible posts are interactable
It doesn't make sense to like, react, reply, etc to something you cannot see and is unexpected for the author of the interacted with post and might make them believe the reacting user actually _can_ see the post. Wrt to fav, reblog, reaction indexes the missing visibility check was also leaking some (presumably/hopefully) low-severity data. Add full-API test for all modes of interactions with private posts.
This commit is contained in:
parent
e3dd94813c
commit
dff532ac72
11 changed files with 347 additions and 8 deletions
|
|
@ -35,7 +35,8 @@ def index_operation do
|
|||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
operationId: "EmojiReactionController.index",
|
||||
responses: %{
|
||||
200 => array_of_reactions_response()
|
||||
200 => array_of_reactions_response(),
|
||||
403 => Operation.response("Access denied", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ def favourite_operation do
|
|||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => status_response(),
|
||||
403 => Operation.response("Access denied", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
|
|
@ -362,6 +363,7 @@ def reblogged_by_operation do
|
|||
"application/json",
|
||||
AccountOperation.array_of_accounts()
|
||||
),
|
||||
403 => Operation.response("Access denied", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ def favorite(%User{} = user, id) do
|
|||
{:ok, _} = res ->
|
||||
res
|
||||
|
||||
{:error, :not_found} = res ->
|
||||
{:error, reason} = res when reason in [:not_found, :forbidden] ->
|
||||
res
|
||||
|
||||
{:error, e} ->
|
||||
|
|
@ -168,6 +168,7 @@ def favorite(%User{} = user, id) do
|
|||
|
||||
def favorite_helper(user, id) do
|
||||
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
|
||||
{_, true} <- {:visible, Visibility.visible_for_user?(object, user)},
|
||||
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
|
||||
{_, {:ok, %Activity{} = activity, _meta}} <-
|
||||
{:common_pipeline,
|
||||
|
|
@ -177,6 +178,9 @@ def favorite_helper(user, id) do
|
|||
{:find_object, _} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:visible, _} ->
|
||||
{:error, :forbidden}
|
||||
|
||||
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
|
||||
if {:object, {"already liked by this actor", []}} in changeset.errors do
|
||||
{:ok, :already_liked}
|
||||
|
|
@ -205,11 +209,15 @@ def unfavorite(id, user) do
|
|||
|
||||
def react_with_emoji(id, user, emoji) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
|
||||
object <- Object.normalize(activity, fetch: false),
|
||||
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:visible, _} ->
|
||||
{:error, dgettext("errors", "Must be able to access post to interact with it")}
|
||||
|
||||
_ ->
|
||||
{:error, dgettext("errors", "Could not add reaction emoji")}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
|
|
@ -108,12 +109,16 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary
|
|||
# it will render it as if it was just the normal referenced post, but use the announce ID for all interaction.
|
||||
# (XXX: fix this in akkoma-fe, then drop such workarounds here and in all other affected places)
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
true <- Visibility.visible_for_user?(activity, draft.user),
|
||||
{_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do
|
||||
%__MODULE__{draft | in_reply_to: activity}
|
||||
else
|
||||
nil ->
|
||||
add_error(draft, dgettext("errors", "Parent post does not exist or was deleted"))
|
||||
|
||||
false ->
|
||||
add_error(draft, dgettext("errors", "Must be able to access post to interact with it"))
|
||||
|
||||
{:type, type} ->
|
||||
add_error(
|
||||
draft,
|
||||
|
|
|
|||
|
|
@ -283,6 +283,7 @@ def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
|
|||
@doc "DELETE /api/v1/statuses/:id"
|
||||
def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||
# CommonAPI already chcks whether user is allowed to delete
|
||||
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
||||
try_render(conn, "show.json",
|
||||
activity: activity,
|
||||
|
|
@ -297,6 +298,7 @@ def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
|
|||
|
||||
@doc "POST /api/v1/statuses/:id/reblog"
|
||||
def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
|
||||
# CommonAPI checks if allowed to reblog
|
||||
with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
|
||||
%Activity{} = announce <- Activity.normalize(announce.data) do
|
||||
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
|
||||
|
|
@ -313,6 +315,7 @@ def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
|
|||
|
||||
@doc "POST /api/v1/statuses/:id/favourite"
|
||||
def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
|
||||
# CommonAPI checks if allowed to fav
|
||||
with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
|
||||
%Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
|
@ -28,6 +29,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
|
||||
with true <- Pleroma.Config.get([:instance, :show_reactions]),
|
||||
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
|
||||
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
|
||||
%Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
|
||||
Object.normalize(activity, fetch: false) do
|
||||
reactions =
|
||||
|
|
@ -37,6 +39,7 @@ def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
|
|||
|
||||
render(conn, "index.json", emoji_reactions: reactions, user: user)
|
||||
else
|
||||
{:visible, _} -> {:error, :forbidden}
|
||||
_e -> json(conn, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -101,6 +104,7 @@ def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) d
|
|||
|> Pleroma.Emoji.fully_qualify_emoji()
|
||||
|> Pleroma.Emoji.maybe_quote()
|
||||
|
||||
# CommonAPI checks if allowed to react
|
||||
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
|
||||
activity = Activity.get_by_id(activity_id)
|
||||
|
||||
|
|
@ -116,6 +120,7 @@ def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) d
|
|||
|> Pleroma.Emoji.fully_qualify_emoji()
|
||||
|> Pleroma.Emoji.maybe_quote()
|
||||
|
||||
# CommonAPI checks only author can revoke reactions
|
||||
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
|
||||
activity = Activity.get_by_id(activity_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ test "the conversation with the blocked user is not marked as unread on a reply"
|
|||
# When it's a reply from the blocked user
|
||||
{:ok, direct2} =
|
||||
CommonAPI.post(blocked, %{
|
||||
status: "reply",
|
||||
status: "@#{third_user.nickname}, #{blocker.nickname} reply",
|
||||
visibility: "direct",
|
||||
in_reply_to_id: direct1.id
|
||||
})
|
||||
|
|
|
|||
|
|
@ -61,8 +61,10 @@ test "it creates or updates a conversation and participations for a given DM" do
|
|||
jafnhar = insert(:user, local: false)
|
||||
tridi = insert(:user)
|
||||
|
||||
to = [har.nickname, jafnhar.nickname, tridi.nickname]
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"})
|
||||
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct", to: to})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
context = object.data["context"]
|
||||
|
|
@ -83,7 +85,8 @@ test "it creates or updates a conversation and participations for a given DM" do
|
|||
CommonAPI.post(jafnhar, %{
|
||||
status: "Hey @#{har.nickname}",
|
||||
visibility: "direct",
|
||||
in_reply_to_status_id: activity.id
|
||||
in_reply_to_status_id: activity.id,
|
||||
to: to
|
||||
})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
|
|
@ -107,7 +110,8 @@ test "it creates or updates a conversation and participations for a given DM" do
|
|||
CommonAPI.post(tridi, %{
|
||||
status: "Hey @#{har.nickname}",
|
||||
visibility: "direct",
|
||||
in_reply_to_status_id: activity.id
|
||||
in_reply_to_status_id: activity.id,
|
||||
to: to
|
||||
})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
|
|
|
|||
|
|
@ -237,6 +237,9 @@ test "filters notifications for Like activities" do
|
|||
user = insert(:user)
|
||||
%{user: other_user, conn: conn} = oauth_access(["read:notifications"])
|
||||
|
||||
{:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(other_user, user)
|
||||
{:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(user, other_user)
|
||||
|
||||
{:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
|
||||
|
||||
{:ok, direct_activity} =
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Workers.ScheduledActivityWorker
|
||||
|
||||
|
|
@ -343,6 +344,54 @@ test "replying to a direct message with visibility other than direct", %{
|
|||
end)
|
||||
end
|
||||
|
||||
test "replying to a post the current user can't access fails", %{user: user, conn: conn} do
|
||||
stranger = insert(:user)
|
||||
|
||||
{:ok, priv_post_act} =
|
||||
CommonAPI.post(stranger, %{status: "forbidden knowledge", visibility: "private"})
|
||||
|
||||
assert Visibility.visible_for_user?(priv_post_act, stranger)
|
||||
refute Visibility.visible_for_user?(priv_post_act, user)
|
||||
|
||||
resp =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses", %{
|
||||
"status" => "@#{stranger.nickname} :peek:",
|
||||
"in_reply_to_id" => priv_post_act.id,
|
||||
"visibility" => "private"
|
||||
})
|
||||
|> json_response_and_validate_schema(422)
|
||||
|
||||
assert match?(%{"error" => _}, resp)
|
||||
end
|
||||
|
||||
test "replying to own DM succeeds", %{user: user, conn: conn} do
|
||||
# this is an "edge" case for visibility: replying user is not part of addressed users (but is the author)
|
||||
stranger = insert(:user)
|
||||
|
||||
{:ok, %{id: dm_id} = dm_post_act} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "@#{stranger.nickname} wanna lose your mind to forbidden knowledge?",
|
||||
visibility: "direct"
|
||||
})
|
||||
|
||||
assert Visibility.visible_for_user?(dm_post_act, stranger)
|
||||
assert Visibility.visible_for_user?(dm_post_act, user)
|
||||
|
||||
resp =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses", %{
|
||||
"status" => "@#{stranger.nickname} :peek:",
|
||||
"in_reply_to_id" => dm_id,
|
||||
"visibility" => "direct"
|
||||
})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert match?(%{"in_reply_to_id" => ^dm_id}, resp)
|
||||
end
|
||||
|
||||
test "replying to a deleted post fails", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
||||
|
|
@ -361,11 +410,11 @@ test "replying to a deleted post fails", %{conn: conn} do
|
|||
json_response_and_validate_schema(conn, 422)
|
||||
end
|
||||
|
||||
test "replying to a non-post activity fails", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
test "replying to a non-post activity fails", %{conn: conn, user: user} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(user, other_user)
|
||||
assert Visibility.visible_for_user?(follow_activity, user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|
|
@ -1293,6 +1342,24 @@ test "author can reblog own private status", %{conn: conn, user: user} do
|
|||
|
||||
assert to_string(activity.id) == id
|
||||
end
|
||||
|
||||
test "cannot reblog private status of others (even if visible)", %{conn: conn, user: user} do
|
||||
followed = insert(:user, local: true)
|
||||
|
||||
{:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(user, followed)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(followed, %{status: "cofe", visibility: "private"})
|
||||
|
||||
assert Visibility.visible_for_user?(activity, user)
|
||||
|
||||
resp =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses/#{activity.id}/reblog")
|
||||
|> json_response_and_validate_schema(404)
|
||||
|
||||
assert match?(%{"error" => _}, resp)
|
||||
end
|
||||
end
|
||||
|
||||
describe "unreblogging" do
|
||||
|
|
@ -1322,6 +1389,33 @@ test "returns 404 error when activity does not exist", %{conn: conn} do
|
|||
|
||||
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
|
||||
end
|
||||
|
||||
test "can’t unreblog someone else’s reblog", %{user: user, conn: conn} do
|
||||
activity = insert(:note_activity)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, other_user)
|
||||
|
||||
# unreblog by base post
|
||||
resp1 =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses/#{activity.id}/unreblog")
|
||||
|> json_response(400)
|
||||
|
||||
assert match?(%{"error" => _}, resp1)
|
||||
|
||||
# unreblog by reblog ID (reblog IDs are accepted by some APIs; ensure it fails here one way or another)
|
||||
resp2 =
|
||||
build_conn()
|
||||
|> assign(:user, user)
|
||||
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write", "read"]))
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses/#{reblog_id}/unreblog")
|
||||
|> json_response_and_validate_schema(404)
|
||||
|
||||
assert match?(%{"error" => _}, resp2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "favoriting" do
|
||||
|
|
@ -1354,6 +1448,21 @@ test "favoriting twice will just return 200", %{conn: conn} do
|
|||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "a status you cannot see fails", %{conn: conn} do
|
||||
stranger = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(stranger, %{status: "it can eternal lie", visibility: "private"})
|
||||
|
||||
resp =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses/#{activity.id}/favourite")
|
||||
|> json_response_and_validate_schema(403)
|
||||
|
||||
assert match?(%{"error" => _}, resp)
|
||||
end
|
||||
|
||||
test "returns 404 error for a wrong id", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|
|
@ -1383,6 +1492,31 @@ test "unfavorites a status and returns it", %{user: user, conn: conn} do
|
|||
assert to_string(activity.id) == id
|
||||
end
|
||||
|
||||
test "doesn't do funny things to other users favs", %{conn: conn} do
|
||||
activity = insert(:note_activity)
|
||||
|
||||
other = insert(:user)
|
||||
{:ok, fav_activity} = CommonAPI.favorite(other, activity.id)
|
||||
|
||||
# using base post ID
|
||||
resp1 =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
|
||||
|> json_response(400)
|
||||
|
||||
assert match?(%{"error" => _}, resp1)
|
||||
|
||||
# some APIs (used to) take IDs of any activity type, make sure this fails one way or another
|
||||
resp2 =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses/#{fav_activity.id}/unfavourite")
|
||||
|> json_response_and_validate_schema(404)
|
||||
|
||||
assert match?(%{"error" => _}, resp2)
|
||||
end
|
||||
|
||||
test "returns 404 error for a wrong id", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|
|
@ -1787,6 +1921,25 @@ test "requires authentication for private posts", %{user: user} do
|
|||
assert id == other_user.id
|
||||
end
|
||||
|
||||
test "fails when base post not visible to current user", %{user: user} do
|
||||
other_user = insert(:user, local: true)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "craving tea and mochi rn",
|
||||
visibility: "private"
|
||||
})
|
||||
|
||||
resp =
|
||||
build_conn()
|
||||
|> assign(:user, other_user)
|
||||
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
|
||||
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|
||||
|> json_response_and_validate_schema(404)
|
||||
|
||||
assert match?(%{"error" => _}, resp)
|
||||
end
|
||||
|
||||
test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
|
||||
clear_config([:instance, :show_reactions], false)
|
||||
|
||||
|
|
@ -1905,6 +2058,25 @@ test "requires authentication for private posts", %{user: user} do
|
|||
|
||||
assert [] == response
|
||||
end
|
||||
|
||||
test "does fail when requesting for a non-visible status", %{user: user} do
|
||||
other_user = insert(:user, local: true)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "deep below it sleeps and mustn't wake",
|
||||
visibility: "private"
|
||||
})
|
||||
|
||||
response =
|
||||
build_conn()
|
||||
|> assign(:user, other_user)
|
||||
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read"]))
|
||||
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|
||||
|> json_response_and_validate_schema(404)
|
||||
|
||||
assert match?(%{"error" => _}, response)
|
||||
end
|
||||
end
|
||||
|
||||
test "context" do
|
||||
|
|
@ -1927,6 +2099,34 @@ test "context" do
|
|||
} = response
|
||||
end
|
||||
|
||||
test "context doesn't leak priv posts" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:statuses"])
|
||||
stranger = insert(:user)
|
||||
|
||||
{:ok, %{id: id1}} = CommonAPI.post(stranger, %{status: "1", visibility: "public"})
|
||||
|
||||
{:ok, %{id: id2}} =
|
||||
CommonAPI.post(stranger, %{status: "2", visibility: "unlisted", in_reply_to_status_id: id1})
|
||||
|
||||
{:ok, %{id: _id_boo} = act_boo} =
|
||||
CommonAPI.post(stranger, %{status: "boo", visibility: "private", in_reply_to_status_id: id1})
|
||||
|
||||
refute Visibility.visible_for_user?(act_boo, user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/api/v1/statuses/#{id1}/context")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert match?(
|
||||
%{
|
||||
"ancestors" => [],
|
||||
"descendants" => [%{"id" => ^id2}]
|
||||
},
|
||||
response
|
||||
)
|
||||
end
|
||||
|
||||
test "context when restrict_unauthenticated is on" do
|
||||
user = insert(:user)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
|
|
|||
|
|
@ -9,10 +9,38 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
defp prepare_reacted_post(visibility \\ "private") do
|
||||
unrelated_user = insert(:user, local: true)
|
||||
poster = insert(:user, local: true)
|
||||
follower = insert(:user, local: true)
|
||||
{:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(follower, poster)
|
||||
|
||||
{:ok, post_activity} = CommonAPI.post(poster, %{status: "miaow!", visibility: visibility})
|
||||
|
||||
if visibility != "direct" do
|
||||
assert Visibility.visible_for_user?(post_activity, follower)
|
||||
end
|
||||
|
||||
if visibility in ["direct", "private"] do
|
||||
refute Visibility.visible_for_user?(post_activity, unrelated_user)
|
||||
end
|
||||
|
||||
{:ok, _react_activity} = CommonAPI.react_with_emoji(post_activity.id, follower, "🐾")
|
||||
|
||||
{post_activity, poster, follower, unrelated_user}
|
||||
end
|
||||
|
||||
defp prepare_conn_of_user(conn, user) do
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write", "read"]))
|
||||
end
|
||||
|
||||
test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
|
@ -120,6 +148,28 @@ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
|
|||
|> json_response_and_validate_schema(400)
|
||||
end
|
||||
|
||||
test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji not allowed for non-visible posts", %{
|
||||
conn: conn
|
||||
} do
|
||||
{%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post()
|
||||
|
||||
# Works for follower
|
||||
resp =
|
||||
prepare_conn_of_user(conn, follower)
|
||||
|> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert match?(%{"id" => ^activity_id}, resp)
|
||||
|
||||
# Fails for stranger
|
||||
resp =
|
||||
prepare_conn_of_user(conn, stranger)
|
||||
|> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|
||||
|> json_response_and_validate_schema(400)
|
||||
|
||||
assert match?(%{"error" => _}, resp)
|
||||
end
|
||||
|
||||
test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
|
@ -194,6 +244,26 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
|
|||
|> json_response(400)
|
||||
end
|
||||
|
||||
test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji only allows original reacter to revoke",
|
||||
%{conn: conn} do
|
||||
{%{id: activity_id} = _activity, author, follower, unrelated} = prepare_reacted_post("public")
|
||||
|
||||
# Works for original reacter
|
||||
prepare_conn_of_user(conn, follower)
|
||||
|> delete("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐾")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
# Fails for anyone else
|
||||
for u <- [author, unrelated] do
|
||||
resp =
|
||||
prepare_conn_of_user(conn, u)
|
||||
|> delete("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐾")
|
||||
|> json_response(400)
|
||||
|
||||
assert match?(%{"error" => _}, resp)
|
||||
end
|
||||
end
|
||||
|
||||
test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
|
@ -275,6 +345,28 @@ test "GET /api/v1/pleroma/statuses/:id/reactions?with_muted=true", %{conn: conn}
|
|||
assert [%{"name" => "🎅", "count" => 2}] = result
|
||||
end
|
||||
|
||||
test "GET /api/v1/pleroma/statuses/:id/reactions not allowed for non-visible posts", %{
|
||||
conn: conn
|
||||
} do
|
||||
{%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post()
|
||||
|
||||
# Works for follower
|
||||
resp =
|
||||
prepare_conn_of_user(conn, follower)
|
||||
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert match?([%{"name" => _, "count" => _} | _], resp)
|
||||
|
||||
# Fails for stranger
|
||||
resp =
|
||||
prepare_conn_of_user(conn, stranger)
|
||||
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions")
|
||||
|> json_response_and_validate_schema(403)
|
||||
|
||||
assert match?(%{"error" => _}, resp)
|
||||
end
|
||||
|
||||
test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do
|
||||
clear_config([:instance, :show_reactions], false)
|
||||
|
||||
|
|
@ -323,4 +415,20 @@ test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
|
|||
|
||||
assert represented_user["id"] == other_user.id
|
||||
end
|
||||
|
||||
test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji not allowed for non-visible posts", %{
|
||||
conn: conn
|
||||
} do
|
||||
{%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post()
|
||||
|
||||
# Works for follower
|
||||
prepare_conn_of_user(conn, follower)
|
||||
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
# Fails for stranger
|
||||
prepare_conn_of_user(conn, stranger)
|
||||
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|
||||
|> json_response_and_validate_schema(403)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue