diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex index 8d6be89a7..60fe8f011 100644 --- a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex +++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex @@ -35,7 +35,8 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation 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 diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 500fbb64e..daf473fa0 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -162,6 +162,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do parameters: [id_param()], responses: %{ 200 => status_response(), + 403 => Operation.response("Access denied", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError) } } @@ -388,6 +389,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do "application/json", AccountOperation.array_of_accounts() ), + 403 => Operation.response("Access denied", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError) } } diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index ae554d0b9..c1719c8d4 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -258,7 +258,7 @@ defmodule Pleroma.Web.CommonAPI do {:ok, _} = res -> res - {:error, :not_found} = res -> + {:error, reason} = res when reason in [:not_found, :forbidden] -> res {:error, e} -> @@ -269,6 +269,7 @@ defmodule Pleroma.Web.CommonAPI do defp 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, @@ -278,6 +279,9 @@ defmodule Pleroma.Web.CommonAPI 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} @@ -311,11 +315,15 @@ defmodule Pleroma.Web.CommonAPI do {:ok, Activity.t()} | {:error, String.t()} 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 diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index f60ed8b02..2d212a443 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -136,22 +136,29 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft - defp in_reply_to(%{params: %{in_reply_to_status_id: :deleted}} = draft) do - add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) - end + defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do + # If a post was deleted all its activities (except the newly added Delete) are purged too, + # thus lookup by Create db ID will yield nil just as if it never existed in the first place. + # + # We allow replying to Announce here, due to a Pleroma-FE quirk where if presented with an Announce id + # it will render it as if it was just the normal referenced post, but use the Announce id for replies + # in the in_reply_to_id key of a POST request to /api/v1/statuses, or as an :id in /api/v1/statuses/:id/*. + # TODO: Fix this quirk in FE and remove here and other affected places + with %Activity{} = activity <- Activity.get_by_id(id), + true <- Visibility.visible_for_user?(activity, draft.user), + {:type, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do + %__MODULE__{draft | in_reply_to: activity} + else + nil -> + add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) - defp in_reply_to(%{params: %{in_reply_to_status_id: id} = params} = draft) when is_binary(id) do - activity = Activity.get_by_id(id) + false -> + add_error(draft, dgettext("errors", "Replying to a status that is not visibile to user")) - params = - if is_nil(activity) do - # Deleted activities are returned as nil - Map.put(params, :in_reply_to_status_id, :deleted) - else - Map.put(params, :in_reply_to_status_id, activity) - end - - in_reply_to(%{draft | params: params}) + {:type, type} -> + add_error(draft, dgettext("errors", "Can only reply to posts, not %{type} activities", + type: inspect(type))) + end end defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 218ee7e28..62cc6a670 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -319,6 +319,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc "DELETE /api/v1/statuses/:id" def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), + # CommonAPI already checks whether user is allowed to delete {:ok, %Activity{}} <- CommonAPI.delete(id, user) do try_render(conn, "show.json", activity: activity, @@ -340,6 +341,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do _ ) do with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params), + # CommonAPI already checks whether user is allowed to reblog %Activity{} = announce <- Activity.normalize(announce.data) do try_render(conn, "show.json", %{activity: announce, for: user, as: :activity}) end @@ -364,6 +366,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do _ ) do with {:ok, _fav} <- CommonAPI.favorite(activity_id, user), + # CommonAPI already checks whether user is allowed to reblog %Activity{} = activity <- Activity.get_by_id(activity_id) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index 662cc15d6..472c3742a 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -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{} = object <- Object.normalize(activity, fetch: false), reactions <- Object.get_emoji_reactions(object) do reactions = @@ -37,6 +39,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do render(conn, "index.json", emoji_reactions: reactions, user: user) else + {:visible, _} -> {:error, :forbidden} _e -> json(conn, []) end end @@ -76,6 +79,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do |> 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) @@ -91,6 +95,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do |> 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) diff --git a/test/pleroma/conversation/participation_test.exs b/test/pleroma/conversation/participation_test.exs index 697bdb7f9..f6fde1875 100644 --- a/test/pleroma/conversation/participation_test.exs +++ b/test/pleroma/conversation/participation_test.exs @@ -332,7 +332,7 @@ defmodule Pleroma.Conversation.ParticipationTest do # 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_conversation_id: blocked_participation.id }) diff --git a/test/pleroma/conversation_test.exs b/test/pleroma/conversation_test.exs index 02b5de615..bc41165ed 100644 --- a/test/pleroma/conversation_test.exs +++ b/test/pleroma/conversation_test.exs @@ -66,8 +66,10 @@ defmodule Pleroma.ConversationTest 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"] @@ -88,7 +90,8 @@ defmodule Pleroma.ConversationTest 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) @@ -112,7 +115,8 @@ defmodule Pleroma.ConversationTest 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) diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs index 88f2fb7af..dd4b7389e 100644 --- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -316,6 +316,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest 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} = diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index c19a6ca4e..d8558465a 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -17,6 +17,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 @@ -267,6 +268,72 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do 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 non-post activity fails", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + assert Visibility.visible_for_user?(follow_activity, user) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "hiiii!", + "in_reply_to_id" => to_string(follow_activity.id) + }) + + assert %{"error" => "Can only reply to posts, not \"Follow\" activities"} = + json_response_and_validate_schema(conn, 422) + end + test "posting a status with an invalid in_reply_to_id", %{conn: conn} do conn = conn @@ -1416,6 +1483,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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(followed, user) + + {: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 @@ -1445,6 +1530,33 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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 @@ -1477,6 +1589,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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 @@ -1506,6 +1633,50 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert to_string(activity.id) == id end + test "can't unfavourite post that isn't favourited", %{conn: conn} do + activity = insert(:note_activity) + + # using base post ID + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unfavourite") + |> json_response(400) + + assert match?(%{"error" => "Could not unfavorite"}, resp) + end + + test "can't unfavourite other user's favs", %{conn: conn} do + activity = insert(:note_activity) + + other = insert(:user) + {:ok, _} = CommonAPI.favorite(activity.id, other) + + # using base post ID + resp = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unfavourite") + |> json_response(400) + + assert match?(%{"error" => "Could not unfavorite"}, resp) + end + + test "can't unfavourite other user's favs using their activity", %{conn: conn} do + activity = insert(:note_activity) + + other = insert(:user) + {:ok, fav_activity} = CommonAPI.favorite(activity.id, other) + # some APIs (used to) take IDs of any activity type, make sure this fails one way or another + resp = + 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" => _}, resp) + end + test "returns 404 error for a wrong id", %{conn: conn} do conn = conn @@ -1959,6 +2130,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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) @@ -2077,6 +2267,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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 @@ -2099,6 +2308,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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 "favorites paginate correctly" do %{user: user, conn: conn} = oauth_access(["read:favourites"]) other_user = insert(:user) diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs index c1e452a1e..f74f5ebc0 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -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(poster, follower) + + {: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 + setup do Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) :ok @@ -137,6 +165,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest 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) @@ -211,6 +261,26 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest 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) @@ -324,6 +394,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do 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) @@ -372,4 +464,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest 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