Merge branch 'akkoma-fixes-1014-1018' into 'develop'

Status visibility checks for post interactions, stop leaking internal Activity representation (Akkoma PR 1014 and 1018)

Closes #3383

See merge request pleroma/pleroma!4400
This commit is contained in:
lain 2025-12-23 13:55:18 +00:00
commit 2f48544937
27 changed files with 1259 additions and 71 deletions

View file

@ -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
})

View file

@ -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)

View file

@ -1580,6 +1580,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert object["content"] == activity["object"]["content"]
end
test "it inserts an incoming reply create activity into the database", %{conn: conn} do
user = insert(:user)
replying_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
data = %{
type: "Create",
object: %{
to: [Pleroma.Constants.as_public(), user.ap_id],
cc: [replying_user.follower_address],
inReplyTo: activity.object.data["id"],
content: "green tea",
type: "Note"
}
}
result =
conn
|> assign(:user, replying_user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{replying_user.nickname}/outbox", data)
|> json_response(201)
updated_object = Object.normalize(activity.object.data["id"], fetch: false)
assert Activity.get_by_ap_id(result["id"])
assert result["object"]
assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
assert object["content"] == data.object.content
assert Pleroma.Web.ActivityPub.Visibility.public?(object)
assert object["inReplyTo"] == activity.object.data["id"]
assert updated_object.data["repliesCount"] == 1
end
test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
user = insert(:user)
@ -1706,6 +1741,289 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert note_object == Object.normalize(note_activity, fetch: false)
end
test "it rejects Add to other user's collection", %{conn: conn} do
user = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Post"})
object = Object.normalize(activity, fetch: false)
object_id = object.data["id"]
data = %{
type: "Add",
target:
"#{Pleroma.Web.Endpoint.url()}/users/#{target_user.nickname}/collections/featured",
object: object_id
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects Remove to other user's collection", %{conn: conn} do
user = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Post"})
object = Object.normalize(activity, fetch: false)
object_id = object.data["id"]
data = %{
type: "Remove",
target:
"#{Pleroma.Web.Endpoint.url()}/users/#{target_user.nickname}/collections/featured",
object: object_id
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects updating Actor's profile", %{conn: conn} do
user = insert(:user, local: true)
user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
user_object_new = Map.put(user_object, "name", "lain")
data = %{
type: "Update",
object: user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
updated_user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
assert updated_user_object == user_object
assert json_response(conn, 400)
end
# Actor publicKey tests are redundant with above test,
# left here for the case that Updating Actors is ever supported
test "it rejects updating Actor's publicKey", %{conn: conn} do
user = insert(:user, local: true)
{:ok, pem} = Pleroma.Keys.generate_rsa_pem()
{:ok, _, public_key} = Pleroma.Keys.keys_from_pem(pem)
# Taken from UserView
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
user_object_public_key = Map.fetch!(user_object, "publicKey")
user_object_public_key = Map.put(user_object_public_key, "publicKeyPem", public_key)
user_object_new = Map.put(user_object, "publicKey", user_object_public_key)
refute user_object == user_object_new
data = %{
type: "Update",
object: user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
new_user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
assert user_object == new_user_object
assert json_response(conn, 400)
end
test "it rejects updating Actor's publicKey of another user", %{conn: conn} do
user = insert(:user)
target_user = insert(:user, local: true)
{:ok, pem} = Pleroma.Keys.generate_rsa_pem()
{:ok, _, public_key} = Pleroma.Keys.keys_from_pem(pem)
# Taken from UserView
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
target_user_object =
Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: target_user})
target_user_object_public_key = Map.fetch!(target_user_object, "publicKey")
target_user_object_public_key =
Map.put(target_user_object_public_key, "publicKeyPem", public_key)
target_user_object_new =
Map.put(target_user_object, "publicKey", target_user_object_public_key)
refute target_user_object == target_user_object_new
data = %{
type: "Update",
object: target_user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{target_user.nickname}/outbox", data)
new_target_user_object =
Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: target_user})
assert target_user_object == new_target_user_object
assert json_response(conn, 403)
end
test "it rejects creating Actors of type Application", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Application"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects creating Actors of type Person", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Person"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects creating Actors of type Service", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Service"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects like activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "Like",
object: %{
id: post_object.data["id"]
}
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it rejects announce activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "Announce",
object: %{
id: post_object.data["id"]
}
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it rejects emojireact activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "EmojiReact",
object: %{
id: post_object.data["id"]
},
content: "😀"
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it increases like count when receiving a like action", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)

View file

@ -9,7 +9,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
@ -530,6 +533,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["generator"])
assert is_nil(modified["object"]["rules"])
assert is_nil(modified["object"]["language"])
assert is_nil(modified["object"]["voters"])
end
test "it strips internal fields of article" do
@ -587,6 +593,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it can handle Listen activities" do
listen_activity = insert(:listen)
# This has an inlined object as in ObjectView
{:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
assert modified["type"] == "Listen"
@ -595,7 +602,36 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
{:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data)
user_ap_id = user.ap_id
activity_ap_id = activity.data["id"]
activity_to = activity.data["to"]
activity_cc = activity.data["cc"]
object_ap_id = activity.data["object"]
object_type = activity.object.data["type"]
# This does not have an inlined object
{:ok, modified2} = Transmogrifier.prepare_outgoing(activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "Listen",
"actor" => ^user_ap_id,
"to" => ^activity_to,
"cc" => ^activity_cc,
"context" => "http://localhost" <> _,
"id" => ^activity_ap_id,
"object" => %{
"actor" => ^user_ap_id,
"attributedTo" => ^user_ap_id,
"id" => ^object_ap_id,
"type" => ^object_type,
"to" => ^activity_to,
"cc" => ^activity_cc
}
},
modified2
)
end
test "custom emoji urls are URI encoded" do
@ -635,6 +671,94 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
} = prepared["object"]
end
test "Updates of Actors are handled" do
user = insert(:user, local: true)
changeset = User.update_changeset(user, %{name: "new name"})
{:ok, unpersisted_user} = Ecto.Changeset.apply_action(changeset, :update)
updated_object =
UserView.render("user.json", user: unpersisted_user)
|> Map.delete("@context")
{:ok, update_data, []} = Builder.update(user, updated_object)
{:ok, activity, _} =
Pipeline.common_pipeline(update_data,
local: true,
user_update_changeset: changeset
)
assert {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
assert prepared["type"] == "Update"
assert prepared["@context"]
assert prepared["object"]["type"] == user.actor_type
end
test "Correctly handles Undo activities" do
blocked = insert(:user)
blocker = insert(:user, local: true)
blocked_ap_id = blocked.ap_id
blocker_ap_id = blocker.ap_id
{:ok, %Activity{} = block_activity} = CommonAPI.block(blocked, blocker)
{:ok, %Activity{} = undo_activity} = CommonAPI.unblock(blocked, blocker)
{:ok, data} = Transmogrifier.prepare_outgoing(undo_activity.data)
block_ap_id = block_activity.data["id"]
assert is_binary(block_ap_id)
assert match?(
%{
"@context" => [_ | _],
"type" => "Undo",
"id" => "http://localhost" <> _,
"actor" => ^blocker_ap_id,
"object" => ^block_ap_id,
"to" => [^blocked_ap_id],
"cc" => [],
"bto" => [],
"bcc" => []
},
data
)
end
test "Correctly handles EmojiReact activities" do
user = insert(:user, local: true)
note_activity = insert(:note_activity)
user_ap_id = user.ap_id
user_followers = user.follower_address
note_author = note_activity.data["actor"]
note_ap_id = note_activity.data["object"]
assert is_binary(note_author)
assert is_binary(note_ap_id)
{:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, "🐈")
{:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "EmojiReact",
"actor" => ^user_ap_id,
"to" => [^user_followers, ^note_author],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"bto" => [],
"bcc" => [],
"content" => "🐈",
"context" => "2hu",
"id" => "http://localhost" <> _,
"object" => ^note_ap_id,
"tag" => []
},
data
)
end
test "it prepares a quote post" do
user = insert(:user)

View file

@ -95,4 +95,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
assert result["object"] == announce.data["id"]
assert result["type"] == "Undo"
end
test "renders a listen activity" do
audio = insert(:audio)
user = insert(:user)
{:ok, listen_activity} = CommonAPI.listen(user, audio.data)
result = ObjectView.render("object.json", %{object: listen_activity})
assert result["id"] == listen_activity.data["id"]
assert result["to"] == listen_activity.data["to"]
assert result["type"] == "Listen"
assert result["object"]["album"] == listen_activity.data["album"]
assert result["object"]["artist"] == listen_activity.data["artist"]
assert result["object"]["length"] == listen_activity.data["length"]
assert result["object"]["title"] == listen_activity.data["title"]
assert result["object"]["type"] == "Audio"
assert result["@context"]
end
end

View file

@ -1086,7 +1086,7 @@ defmodule Pleroma.Web.CommonAPITest do
test "only public can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "private status", visibility: "private"})
{:error, :visibility_error} = CommonAPI.pin(activity.id, user)
{:error, :non_public_error} = CommonAPI.pin(activity.id, user)
end
test "unpin status", %{user: user, activity: activity} do
@ -1300,6 +1300,47 @@ defmodule Pleroma.Web.CommonAPITest do
} = flag_activity
end
test "doesn't create a report when post is not visible to user" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, post} = CommonAPI.post(target_user, %{status: "Eric", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, reporter)
# Fails when all status are invisible
report_data = %{
account_id: target_user.id,
comment: "foobar",
status_ids: [post.id]
}
assert {:error, :visibility_error} = CommonAPI.report(reporter, report_data)
end
test "doesn't create a report when some posts are not visible to user" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, visible_activity} = CommonAPI.post(target_user, %{status: "cofe"})
{:ok, invisibile_activity} =
CommonAPI.post(target_user, %{status: "cawfee", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(invisibile_activity)
assert Pleroma.Web.ActivityPub.Visibility.public?(visible_activity)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(invisibile_activity, reporter)
# Fails when some statuses are invisible
report_data_partial = %{
account_id: target_user.id,
comment: "foobar",
status_ids: [visible_activity.id, invisibile_activity.id]
}
assert {:error, :visibility_error} = CommonAPI.report(reporter, report_data_partial)
end
test "updates report state" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)

View file

@ -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} =

View file

@ -147,7 +147,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
|> json_response_and_validate_schema(400)
end
test "returns error when account is not exist", %{
test "returns error when account does not exist", %{
conn: conn,
activity: activity
} do
@ -159,6 +159,51 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"}
end
test "returns not found when post isn't visible to reporter", %{user: target_user} do
%{conn: conn, user: reporter} = oauth_access(["write:reports"])
{:ok, invisible_activity} =
CommonAPI.post(target_user, %{status: "Invisible!", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(invisible_activity)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(invisible_activity, reporter)
assert %{"error" => "Record not found"} =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/reports",
%{"account_id" => target_user.id, "status_ids" => [invisible_activity.id]}
)
|> json_response_and_validate_schema(404)
end
test "returns not found when some post aren't visible to reporter", %{
activity: activity,
user: target_user
} do
%{conn: conn, user: reporter} = oauth_access(["write:reports"])
{:ok, invisible_activity} =
CommonAPI.post(target_user, %{status: "Invisible!", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(invisible_activity)
assert Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, reporter)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(invisible_activity, reporter)
assert %{"error" => "Record not found"} =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/reports",
%{
"account_id" => target_user.id,
"status_ids" => [activity.id, invisible_activity.id]
}
)
|> json_response_and_validate_schema(404)
end
test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do
insert(:user, %{is_admin: true, email: nil})

View file

@ -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,73 @@ 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 +1484,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 +1531,34 @@ 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,13 +1591,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(200)
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
test "a status you cannot see fails", %{conn: conn} do
stranger = insert(:user)
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
{:ok, activity} =
CommonAPI.post(stranger, %{status: "it can eternal lie", visibility: "private"})
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "returns 404 error for a wrong id", %{conn: conn} do
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
end
@ -1506,6 +1630,54 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id
end
test "can't unfavourite post that isn't visible to user" do
user = insert(:user)
%{conn: conn, user: stranger} = oauth_access(["write:favourites"])
{:ok, activity} = CommonAPI.post(user, %{status: "invisible", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "can't unfavourite post that isn't favourited", %{conn: conn} do
activity = insert(:note_activity)
# using base post ID
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
|> json_response_and_validate_schema(400) == %{"error" => "Could not unfavorite"}
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
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
|> json_response_and_validate_schema(400) == %{"error" => "Could not unfavorite"}
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
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{fav_activity.id}/unfavourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
@ -1516,6 +1688,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
end
test "can't favourite post that isn't visible to user" do
user = insert(:user)
%{conn: conn, user: stranger} = oauth_access(["write:favourites"])
{:ok, activity} = CommonAPI.post(user, %{status: "invisible", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
describe "pinned statuses" do
setup do: oauth_access(["write:accounts"])
@ -1549,7 +1734,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response(403) == %{"error" => "Invalid credentials."}
end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
test "/pin: returns 422 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
conn =
@ -1562,6 +1747,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
}
end
test "/pin: returns 404 error when activity not visible to user", %{user: user} do
%{conn: conn, user: stranger} = oauth_access(["write:accounts"])
{:ok, activity} = CommonAPI.post(user, %{status: "invisible", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "pin by another user", %{activity: activity} do
%{conn: conn} = oauth_access(["write:accounts"])
@ -1596,6 +1793,32 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "/unpin: returns 404 error when activity not visible to user", %{user: user} do
%{conn: conn, user: stranger} = oauth_access(["write:accounts"])
{:ok, activity} = CommonAPI.post(user, %{status: "yumi", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "/unpin: returns 422 error when activity not owned by user", %{activity: activity} do
%{conn: conn, user: user} = oauth_access(["write:accounts"])
assert Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, user)
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response_and_validate_schema(422) == %{
"error" => "Someone else's status cannot be unpinned"
}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
@ -1707,6 +1930,28 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
json_response_and_validate_schema(bookmarks, 200)
end
test "cannot bookmark invisible post" do
user = insert(:user)
%{conn: conn, user: stranger} = oauth_access(["write:bookmarks"])
{:ok, activity} = CommonAPI.post(user, %{status: "mocha", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
resp1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/bookmark")
assert json_response_and_validate_schema(resp1, 404) == %{"error" => "Record not found"}
resp2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unbookmark")
assert json_response_and_validate_schema(resp2, 404) == %{"error" => "Record not found"}
end
test "bookmark folders" do
%{conn: conn, user: user} = oauth_access(["write:bookmarks", "read:bookmarks"])
@ -1804,6 +2049,30 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response_and_validate_schema(200)
end
test "cannot mute not visible conversation", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "Invisible!", visibility: "private"})
%{conn: conn} = oauth_access(["write:mutes"])
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
|> json_response_and_validate_schema(404) == %{
"error" => "Record not found"
}
end
test "cannot unmute not visible conversation", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "Invisible!", visibility: "private"})
%{conn: conn} = oauth_access(["write:mutes"])
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response_and_validate_schema(404) == %{
"error" => "Record not found"
}
end
end
test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
@ -1970,6 +2239,22 @@ 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)
%{conn: conn} = oauth_access(["read:accounts"])
{:ok, activity} =
CommonAPI.post(user, %{
status: "craving tea and mochi rn",
visibility: "private"
})
assert conn
|> assign(:user, other_user)
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
clear_config([:instance, :show_reactions], false)
@ -2088,6 +2373,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
@ -2110,6 +2414,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)

View file

@ -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(404)
assert match?(%{"error" => "Record not found"}, 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,25 @@ 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
assert prepare_conn_of_user(conn, stranger)
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do
clear_config([:instance, :show_reactions], false)
@ -372,4 +461,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
assert prepare_conn_of_user(conn, follower)
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|> json_response_and_validate_schema(200)
# Fails for stranger
assert prepare_conn_of_user(conn, stranger)
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
end