akkoma/test/pleroma/web/common_api_test.exs
Oneric 865cfabf88 Drop conversation addressing
No known client ever used this. Currently among akkoma-fe, pleroma-fe,
Husky, Mangane and pl-fe only the latter acknowledes the existence of
the in_reply_to_conversation_id paramter in its API definitions,
but even pl-fe does never actually use the parameter anywhere.

Since the API parameter already was converted to DMs internally,
we do not need to make special considerations for already existing
old conversation-addressed posts. Since they share the context they
should also continue to show up in the intended thread anyway.

The pleroma.participants subkey mentioned in docs did already not exist
prior to this commit. Instead the accounts key doesn’t automatically update
and this affects conversations retrieved from the Mastodon API endpoint too
(which may be considered a pre-existing bug).

If desired clients can already avoid unintended participant additions
by using the explicit-addressing feature originally introduced in
https://git.pleroma.social/pleroma/pleroma/-/merge_requests/1239.
With the above-mentioned feature/bug of conversation participants
not updating automatically it can replace almost everything
conversation addressing was able to do. The sole exception being
creating new non-reply posts in the same context.
Neither conversation addressing nor explicit addressing
achieves robust, federated group chats though.

Resolves: https://akkoma.dev/AkkomaGang/akkoma/issues/812
2025-11-17 00:00:00 +00:00

1482 lines
47 KiB
Elixir

# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPITest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase, async: false
@moduletag :mocked
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.PollWorker
import Pleroma.Factory
import Mock
import Ecto.Query, only: [from: 2]
require Pleroma.Constants
setup_all do
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
clear_config([Pleroma.Uploaders.Local, :uploads], "uploads")
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :safe_dm_mentions])
setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses])
describe "posting polls" do
test "it posts a poll" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "who is the best",
poll: %{expires_in: 600, options: ["reimu", "marisa"]}
})
object = Object.normalize(activity, fetch: false)
assert object.data["type"] == "Question"
assert object.data["oneOf"] |> length() == 2
{:ok, time, _} = DateTime.from_iso8601(object.data["closed"])
assert_enqueued(
worker: PollWorker,
args: %{op: "poll_end", activity_id: activity.id},
scheduled_at: time
)
end
end
describe "blocking" do
setup do
blocker = insert(:user)
blocked = insert(:user, local: false)
CommonAPI.follow(blocker, blocked)
CommonAPI.follow(blocked, blocker)
CommonAPI.accept_follow_request(blocker, blocked)
CommonAPI.accept_follow_request(blocked, blocked)
%{blocker: blocker, blocked: blocked}
end
test "it blocks and federates", %{blocker: blocker, blocked: blocked} do
clear_config([:instance, :federating], true)
clear_config([:activitypub, :outgoing_blocks], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert User.get_follow_state(blocker, blocked) == :follow_accept
refute is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked))
assert {:ok, block} = CommonAPI.block(blocker, blocked)
assert block.local
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
refute User.get_follow_state(blocker, blocked)
assert %{data: %{"state" => "reject"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked)
assert called(Pleroma.Web.Federator.publish(block))
end
end
test "it blocks and does not federate if outgoing blocks are disabled", %{
blocker: blocker,
blocked: blocked
} do
clear_config([:instance, :federating], true)
clear_config([:activitypub, :outgoing_blocks], false)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, block} = CommonAPI.block(blocker, blocked)
assert block.local
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
refute called(Pleroma.Web.Federator.publish(block))
end
end
end
describe "unblocking" do
test "it works even without an existing block activity" do
blocked = insert(:user)
blocker = insert(:user)
User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked)
refute User.blocks?(blocker, blocked)
end
test "it unblocks and does not federate if outgoing blocks are disabled" do
clear_config([:instance, :federating], true)
clear_config([:activitypub, :outgoing_blocks], false)
blocked = insert(:user)
blocker = insert(:user)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, block} = CommonAPI.block(blocker, blocked)
assert block.local
assert User.blocks?(blocker, blocked)
assert {:ok, unblock} = CommonAPI.unblock(blocker, blocked)
assert unblock.local
refute User.blocks?(blocker, blocked)
assert_not_called(Pleroma.Web.Federator.publish(:_))
end
end
end
describe "deletion" do
test "it works with pruned objects" do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
clear_config([:instance, :federating], true)
Object.normalize(post, fetch: false)
|> Object.prune()
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, user)
assert delete.local
assert called(Pleroma.Web.Federator.publish(delete))
end
refute Activity.get_by_id(post.id)
end
test "it allows users to delete their posts" do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, user)
assert delete.local
assert called(Pleroma.Web.Federator.publish(delete))
end
refute Activity.get_by_id(post.id)
end
test "it does not allow a user to delete their posts" do
user = insert(:user)
other_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user)
assert Activity.get_by_id(post.id)
end
test "it allows moderators to delete other user's posts" do
user = insert(:user)
moderator = insert(:user, is_moderator: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
test "it allows admins to delete other user's posts" do
user = insert(:user)
moderator = insert(:user, is_admin: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
test "superusers deleting non-local posts won't federate the delete" do
# This is the user of the ingested activity
_user =
insert(:user,
local: false,
ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: NaiveDateTime.utc_now()
)
moderator = insert(:user, is_admin: true)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
{:ok, post} = Transmogrifier.handle_incoming(data)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute called(Pleroma.Web.Federator.publish(:_))
end
refute Activity.get_by_id(post.id)
end
test "it allows privileged users to delete banned user's posts" do
clear_config([:instance, :moderator_privileges], [:messages_delete])
user = insert(:user)
moderator = insert(:user, is_moderator: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
User.set_activation(user, false)
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
end
test "favoriting race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
users_serial
|> Enum.map(fn user ->
CommonAPI.favorite(user, activity.id)
end)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 10
users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.favorite(user, activity.id)
end)
end)
|> Enum.map(&Task.await/1)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 20
end
test "repeating race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
users_serial
|> Enum.map(fn user ->
CommonAPI.repeat(activity.id, user)
end)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 10
users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.repeat(activity.id, user)
end)
end)
|> Enum.map(&Task.await/1)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 20
end
test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
clear_config([:instance, :safe_dm_mentions], true)
{:ok, activity} =
CommonAPI.post(har, %{
status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again",
visibility: "direct"
})
refute tridi.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
end
test "it de-duplicates tags" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"})
object = Object.normalize(activity, fetch: false)
assert Object.tags(object) == ["2hu"]
end
test "it adds emoji in the object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"})
assert Object.normalize(activity, fetch: false).data["emoji"]["firefox"]
end
describe "posting" do
test "it adds an emoji on an external site" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey :external_emoji:"})
assert %{"external_emoji" => url} = Object.normalize(activity).data["emoji"]
assert url == "https://example.com/emoji.png"
{:ok, activity} = CommonAPI.post(user, %{status: "hey :blank:"})
assert %{"blank" => url} = Object.normalize(activity).data["emoji"]
assert url == "#{Pleroma.Web.Endpoint.url()}/emoji/blank.png"
end
test "it copies emoji from the subject of the parent post" do
%Object{} =
object =
Object.normalize("https://patch.cx/objects/a399c28e-c821-4820-bc3e-4afeb044c16f",
fetch: true
)
activity = Activity.get_create_by_object_ap_id(object.data["id"])
user = insert(:user)
{:ok, reply_activity} =
CommonAPI.post(user, %{
in_reply_to_id: activity.id,
status: ":joker_disapprove:",
spoiler_text: ":joker_smile:"
})
assert Object.normalize(reply_activity).data["emoji"]["joker_smile"]
refute Object.normalize(reply_activity).data["emoji"]["joker_disapprove"]
end
test "deactivated users can't post" do
user = insert(:user, is_active: false)
assert {:error, _} = CommonAPI.post(user, %{status: "ye"})
end
test "it supports explicit addressing" do
user = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
user_four = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status:
"Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.",
to: [user_two.nickname, user_four.nickname, "nonexistent"]
})
assert user.ap_id in activity.recipients
assert user_two.ap_id in activity.recipients
assert user_four.ap_id in activity.recipients
refute user_three.ap_id in activity.recipients
end
test "it filters out obviously bad tags when accepting a post as HTML" do
user = insert(:user)
post = "<p><b>2hu</b></p><script>alert('xss')</script>"
{:ok, activity} =
CommonAPI.post(user, %{
status: post,
content_type: "text/html"
})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)"
assert object.data["source"] == %{
"mediaType" => "text/html",
"content" => post
}
end
test "it filters out obviously bad tags when accepting a post as Markdown" do
user = insert(:user)
post = "<p><b>2hu</b></p><script>alert('xss')</script>"
{:ok, activity} =
CommonAPI.post(user, %{
status: post,
content_type: "text/markdown"
})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "<p><b>2hu</b></p>"
assert object.data["source"] == %{
"mediaType" => "text/markdown",
"content" => post
}
end
test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
assert {:ok, _} =
CommonAPI.post(user, %{
status: "suya..",
visibility: "direct",
in_reply_to_status_id: activity.id
})
Enum.each(["public", "private", "unlisted"], fn visibility ->
assert {:error, "The message visibility must be direct"} =
CommonAPI.post(user, %{
status: "suya..",
visibility: visibility,
in_reply_to_status_id: activity.id
})
end)
end
test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"})
{:ok, open_answer} =
CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id})
# The OP is implicitly added
assert user.ap_id in open_answer.recipients
{:ok, secret_answer} =
CommonAPI.post(other_user, %{
status: "lol, that guy really is stupid, right, @#{third_user.nickname}?",
in_reply_to_status_id: post.id,
visibility: "direct"
})
assert third_user.ap_id in secret_answer.recipients
# The OP is not added
refute user.ap_id in secret_answer.recipients
end
test "it adds the htmlMFM term to MFM posts and properly processes it" do
user = insert(:user)
assert {:ok,
%Pleroma.Activity{
object: %Pleroma.Object{
data: %{
"content" => content,
"source" => %{
"content" => source_content,
"mediaType" => "text/x.misskeymarkdown"
},
"htmlMfm" => html_mfm
}
}
}} =
CommonAPI.post(user, %{
status: "<p class='scrub-this'>$[spin 13:37]</p>",
content_type: "text/x.misskeymarkdown"
})
assert html_mfm == true
assert content =~ "mfm-spin"
assert content =~ "13:37"
refute content =~ "scrub-this"
assert source_content == "<p class='scrub-this'>$[spin 13:37]</p>"
end
test "it returns error when status is empty and no attachments" do
user = insert(:user)
assert {:error, "Cannot post an empty status without attachments"} =
CommonAPI.post(user, %{status: ""})
end
test "it validates character limits are correctly enforced" do
clear_config([:instance, :limit], 5)
user = insert(:user)
assert {:error, "The status is over the character limit"} =
CommonAPI.post(user, %{status: "foobar"})
assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"})
end
test "it can handle activities that expire" do
user = insert(:user)
expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000})
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: activity.id},
scheduled_at: expires_at
)
end
end
describe "reactions" do
test "reacting to a status with an emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
assert reaction.data["actor"] == user.ap_id
assert reaction.data["content"] == "👍"
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".")
end
test "unreacting to a status with an emoji" do
user = insert(:user)
other_user = insert(:user)
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
{:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert unreaction.data["type"] == "Undo"
assert unreaction.data["object"] == reaction.data["id"]
assert unreaction.local
# On federation, it contains the undone (and deleted) object
unreaction_with_object = %{
unreaction
| data: Map.put(unreaction.data, "object", reaction.data)
}
assert called(Pleroma.Web.Federator.publish(unreaction_with_object))
end
end
test "repeating a status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user)
assert Visibility.is_public?(announce_activity)
end
test "can't repeat a repeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user)
refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user))
end
test "repeating a status privately" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce_activity} =
CommonAPI.repeat(activity.id, user, %{visibility: "private"})
assert Visibility.is_private?(announce_activity)
refute Visibility.visible_for_user?(announce_activity, nil)
end
test "author can repeat own private statuses" do
author = insert(:user)
follower = insert(:user)
CommonAPI.follow(follower, author)
{:ok, activity} = CommonAPI.post(author, %{status: "cofe", visibility: "private"})
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, author)
assert Visibility.is_private?(announce_activity)
refute Visibility.visible_for_user?(announce_activity, nil)
assert Visibility.visible_for_user?(activity, follower)
assert {:error, :not_found} = CommonAPI.repeat(activity.id, follower)
end
test "favoriting a status" do
user = insert(:user)
other_user = insert(:user)
{:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id)
assert data["type"] == "Like"
assert data["actor"] == user.ap_id
assert data["object"] == post_activity.data["object"]
end
test "retweeting a status twice returns the status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user)
{:ok, ^announce} = CommonAPI.repeat(activity.id, user)
end
test "favoriting a status twice returns ok, but without the like activity" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id)
end
end
describe "pinned statuses" do
setup do
clear_config([:instance, :max_pinned_statuses], 1)
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
[user: user, activity: activity]
end
test "activity not found error", %{user: user} do
assert {:error, :not_found} = CommonAPI.pin("id", user)
end
test "pin status", %{user: user, activity: activity} do
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
%{data: %{"id" => object_id}} = Object.normalize(activity)
user = refresh_record(user)
assert user.pinned_objects |> Map.keys() == [object_id]
end
test "pin poll", %{user: user} do
{:ok, activity} =
CommonAPI.post(user, %{
status: "How is fediverse today?",
poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20}
})
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
%{data: %{"id" => object_id}} = Object.normalize(activity)
user = refresh_record(user)
assert user.pinned_objects |> Map.keys() == [object_id]
end
test "unlisted statuses can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"})
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
end
test "only self-authored can be pinned", %{activity: activity} do
user = insert(:user)
assert {:error, :ownership_error} = CommonAPI.pin(activity.id, user)
end
test "max pinned statuses", %{user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
assert {:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity_two.id, user)
end
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)
end
test "unpin status", %{user: user, activity: activity} do
{:ok, activity} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
id = activity.id
assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user))
user = refresh_record(user)
assert user.pinned_objects == %{}
end
test "should unpin when deleting a status", %{user: user, activity: activity} do
{:ok, activity} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
assert {:ok, _} = CommonAPI.delete(activity.id, user)
user = refresh_record(user)
assert user.pinned_objects == %{}
end
test "ephemeral activity won't be deleted if was pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "Hello!", expires_in: 601})
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
{:ok, _activity} = CommonAPI.pin(activity.id, user)
refute Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
user = refresh_record(user)
{:ok, _} = CommonAPI.unpin(activity.id, user)
# recreates expiration job on unpin
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
end
test "ephemeral activity deletion job won't be deleted on pinning error", %{
user: user,
activity: activity
} do
clear_config([:instance, :max_pinned_statuses], 1)
{:ok, _activity} = CommonAPI.pin(activity.id, user)
{:ok, activity2} = CommonAPI.post(user, %{status: "another status", expires_in: 601})
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
user = refresh_record(user)
{:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity2.id, user)
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
end
end
describe "mute tests" do
setup do
user = insert(:user)
activity = insert(:note_activity)
[user: user, activity: activity]
end
test "marks notifications as read after mute" do
author = insert(:user)
activity = insert(:note_activity, user: author)
friend1 = insert(:user)
friend2 = insert(:user)
{:ok, reply_activity} =
CommonAPI.post(
friend2,
%{
status: "@#{author.nickname} @#{friend1.nickname} test reply",
in_reply_to_status_id: activity.id
}
)
{:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id)
{:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1)
assert Repo.aggregate(
from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
:count
) == 1
unread_notifications =
Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id))
assert Enum.any?(unread_notifications, fn n ->
n.type == "favourite" && n.activity_id == favorite_activity.id
end)
assert Enum.any?(unread_notifications, fn n ->
n.type == "reblog" && n.activity_id == repeat_activity.id
end)
assert Enum.any?(unread_notifications, fn n ->
n.type == "mention" && n.activity_id == reply_activity.id
end)
{:ok, _} = CommonAPI.add_mute(author, activity)
assert CommonAPI.thread_muted?(author, activity)
assert Repo.aggregate(
from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
:count
) == 1
read_notifications =
Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id))
assert Enum.any?(read_notifications, fn n ->
n.type == "favourite" && n.activity_id == favorite_activity.id
end)
assert Enum.any?(read_notifications, fn n ->
n.type == "reblog" && n.activity_id == repeat_activity.id
end)
assert Enum.any?(read_notifications, fn n ->
n.type == "mention" && n.activity_id == reply_activity.id
end)
end
test "add mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
assert CommonAPI.thread_muted?(user, activity)
end
test "add expiring mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity, %{expires_in: 60})
assert CommonAPI.thread_muted?(user, activity)
worker = Pleroma.Workers.MuteExpireWorker
args = %{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id}
assert_enqueued(
worker: worker,
args: args
)
assert :ok = perform_job(worker, args)
refute CommonAPI.thread_muted?(user, activity)
end
test "remove mute", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:ok, _} = CommonAPI.remove_mute(user, activity)
refute CommonAPI.thread_muted?(user, activity)
end
test "remove mute by ids", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:ok, _} = CommonAPI.remove_mute(user.id, activity.id)
refute CommonAPI.thread_muted?(user, activity)
end
test "check that mutes can't be duplicate", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:error, _} = CommonAPI.add_mute(user, activity)
end
end
describe "reports" do
test "creates a report" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"})
reporter_ap_id = reporter.ap_id
target_ap_id = target_user.ap_id
activity_ap_id = activity.data["id"]
comment = "foobar"
report_data = %{
account_id: target_user.id,
comment: comment,
status_ids: [activity.id]
}
note_obj = %{
"type" => "Note",
"id" => activity_ap_id,
"content" => "foobar",
"published" => activity.object.data["published"],
"actor" => AccountView.render("show.json", %{user: target_user})
}
assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^comment,
"object" => [^target_ap_id, ^note_obj],
"state" => "open"
}
} = flag_activity
end
test "updates report state" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
[reported_user, activity_id] = report.data["object"]
assert reported_user == target_user.ap_id
assert activity_id == activity.data["id"]
end
test "does not update report state when state is unsupported" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end
test "updates state of multiple reports" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: first_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, %Activity{id: second_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel very offended!",
status_ids: [activity.id]
})
{:ok, report_ids} =
CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
first_report = Activity.get_by_id(first_report_id)
second_report = Activity.get_by_id(second_report_id)
assert report_ids -- [first_report_id, second_report_id] == []
assert first_report.data["state"] == "resolved"
assert second_report.data["state"] == "resolved"
end
end
describe "reblog muting" do
setup do
muter = insert(:user)
muted = insert(:user)
[muter: muter, muted: muted]
end
test "add a reblog mute", %{muter: muter, muted: muted} do
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == false
end
test "remove a reblog mute", %{muter: muter, muted: muted} do
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
{:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == true
end
end
describe "follow/2" do
test "directly follows a non-locked local user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
assert User.following?(follower, followed)
end
test "directly follows back a locked, but followback-allowing local user" do
uopen = insert(:user, is_locked: false)
uselective = insert(:user, is_locked: true, permit_followback: true)
assert {:ok, uselective, uopen, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(uselective, uopen)
assert User.get_follow_state(uselective, uopen) == :follow_accept
assert {:ok, uopen, uselective, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(uopen, uselective)
assert User.get_follow_state(uopen, uselective) == :follow_accept
end
test "creates a pending request for locked, non-followback local user" do
uopen = insert(:user, is_locked: false)
ulocked = insert(:user, is_locked: true, permit_followback: false)
assert {:ok, ulocked, uopen, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(ulocked, uopen)
assert User.get_follow_state(ulocked, uopen) == :follow_accept
assert {:ok, uopen, ulocked, %{data: %{"state" => "pending"}}} =
CommonAPI.follow(uopen, ulocked)
assert User.get_follow_state(uopen, ulocked) == :follow_pending
end
end
describe "unfollow/2" do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
{:ok, _subscription} = User.subscribe(follower, followed)
assert User.subscribed_to?(follower, followed)
{:ok, follower} = CommonAPI.unfollow(follower, followed)
refute User.subscribed_to?(follower, followed)
end
test "removes a pending follow for a local user" do
follower = insert(:user)
followed = insert(:user, is_locked: true)
assert {:ok, follower, followed, %{id: _activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(follower, followed)
assert User.get_follow_state(follower, followed) == :follow_pending
assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
assert User.get_follow_state(follower, followed) == nil
assert is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed))
assert %{
data: %{
"type" => "Undo",
"object" => %{"type" => "Follow"}
}
} = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
end
test "cancels a pending follow for a remote user" do
follower = insert(:user)
followed = insert(:user, is_locked: true, local: false)
assert {:ok, follower, followed, %{id: _activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(follower, followed)
assert User.get_follow_state(follower, followed) == :follow_pending
assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
assert User.get_follow_state(follower, followed) == nil
assert is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed))
assert %{
data: %{
"type" => "Undo",
"object" => %{"type" => "Follow"}
}
} = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
end
end
describe "accept_follow_request/2" do
test "after acceptance, it sets all existing pending follow request states to 'accept'" do
user = insert(:user, is_locked: true)
follower = insert(:user)
follower_two = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
{:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
assert follow_activity_three.data["state"] == "pending"
{:ok, _follower} = CommonAPI.accept_follow_request(follower, user)
assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
test "after rejection, it sets all existing pending follow request states to 'reject'" do
user = insert(:user, is_locked: true)
follower = insert(:user)
follower_two = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
{:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
assert follow_activity_three.data["state"] == "pending"
{:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
assert Repo.get(Activity, follow_activity.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
test "doesn't create a following relationship if the corresponding follow request doesn't exist" do
user = insert(:user, is_locked: true)
not_follower = insert(:user)
CommonAPI.accept_follow_request(not_follower, user)
assert Pleroma.FollowingRelationship.following?(not_follower, user) == false
end
end
describe "vote/3" do
test "does not allow to vote twice" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Am I cute?",
poll: %{options: ["Yes", "No"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
{:ok, _, object} = CommonAPI.vote(other_user, object, [0])
assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1])
end
end
describe "get_user/1" do
test "gets user by ap_id" do
user = insert(:user)
assert CommonAPI.get_user(user.ap_id) == user
end
test "gets user by guessed nickname" do
user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom")
assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user
end
test "fallback" do
assert %User{
name: "",
ap_id: "",
nickname: "erroruser@example.com"
} = CommonAPI.get_user("")
end
end
describe "with `local` visibility" do
setup do: clear_config([:instance, :federating], true)
test "post" do
user = insert(:user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
assert Visibility.is_local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity))
end
end
test "delete" do
user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} =
CommonAPI.delete(activity_id, user)
assert Visibility.is_local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity))
end
end
test "repeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} =
CommonAPI.repeat(activity_id, user)
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unrepeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
assert {:ok, _} = CommonAPI.repeat(activity_id, user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unrepeat(activity_id, user)
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "favorite" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} =
CommonAPI.favorite(user, activity.id)
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unfavorite" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
{:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "react_with_emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} =
CommonAPI.react_with_emoji(activity.id, user, "👍")
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unreact_with_emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
{:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
end
describe "update/3" do
test "updates a post" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1"})
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Map.has_key?(updated_object.data, "updated")
end
test "does not change visibility" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1", visibility: "private"})
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Visibility.get_visibility(updated_object) == "private"
assert Visibility.get_visibility(updated) == "private"
end
test "updates a post with emoji" do
[{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all()
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"})
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2 :#{emoji2}:"
assert %{^emoji2 => _} = updated_object.data["emoji"]
end
test "updates a post with emoji and federate properly" do
[{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all()
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"})
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _p -> nil end do
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"})
assert updated.data["object"]["content"] == "updated 2 :#{emoji2}:"
assert %{^emoji2 => _} = updated.data["object"]["emoji"]
assert called(Pleroma.Web.Federator.publish(updated))
end
end
test "editing a post that copied a remote title with remote emoji should keep that emoji" do
remote_emoji_uri = "https://remote.org/emoji.png"
note =
insert(
:note,
data: %{
"summary" => ":remoteemoji:",
"emoji" => %{
"remoteemoji" => remote_emoji_uri
},
"tag" => [
%{
"type" => "Emoji",
"name" => "remoteemoji",
"icon" => %{"url" => remote_emoji_uri}
}
]
}
)
note_activity = insert(:note_activity, note: note)
user = insert(:user)
{:ok, reply} =
CommonAPI.post(user, %{
status: "reply",
spoiler_text: ":remoteemoji:",
in_reply_to_id: note_activity.id
})
assert reply.object.data["emoji"]["remoteemoji"] == remote_emoji_uri
{:ok, edit} =
CommonAPI.update(user, reply, %{status: "reply mew mew", spoiler_text: ":remoteemoji:"})
edited_note = Pleroma.Object.normalize(edit)
assert edited_note.data["emoji"]["remoteemoji"] == remote_emoji_uri
end
test "respects MRF" do
user = insert(:user)
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
clear_config([:mrf_keyword, :replace], [{"updated", "mewmew"}])
{:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "updated 1"})
assert Object.normalize(activity).data["summary"] == "mewmew 1"
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "mewmew 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Map.has_key?(updated_object.data, "updated")
end
end
end