akkoma/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs

2532 lines
81 KiB
Elixir
Raw Normal View History

# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
use Pleroma.Web.ConnCase, async: false
2020-08-22 17:46:01 +00:00
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
2021-07-18 01:35:35 +00:00
alias Pleroma.Workers.ScheduledActivityWorker
import Pleroma.Factory
setup do: clear_config([:instance, :federating])
setup do: clear_config([:instance, :allow_relay])
setup do: clear_config([:rich_media, :enabled])
setup do: clear_config([:mrf, :policies])
setup do: clear_config([:mrf_keyword, :reject])
setup do: clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
setup do: clear_config([Pleroma.Uploaders.Local, :uploads], "uploads")
2019-10-14 06:31:08 +00:00
describe "posting statuses" do
setup do: oauth_access(["write:statuses"])
2019-10-19 23:23:13 +00:00
test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
clear_config([:instance, :federating], true)
2020-08-22 17:46:01 +00:00
Config.get([:instance, :allow_relay], true)
response =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"content_type" => "text/plain",
"source" => "Pleroma FE",
"status" => "Hello world",
"visibility" => "public"
})
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
assert response["reblogs_count"] == 0
ObanHelpers.perform_all()
response =
conn
|> get("/api/v1/statuses/#{response["id"]}", %{})
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
assert response["reblogs_count"] == 0
end
test "posting a status", %{conn: conn} do
idempotency_key = "Pikachu rocks!"
conn_one =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "0",
"language" => "ja"
})
assert %{
"content" => "cofe",
"id" => id,
"spoiler_text" => "2hu",
"sensitive" => false,
"language" => "ja"
} = json_response_and_validate_schema(conn_one, 200)
assert Activity.get_by_id(id)
conn_two =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => 0
})
2021-02-16 23:45:01 +00:00
# Idempotency plug response means detection fail
assert %{"id" => second_id} = json_response(conn_two, 200)
assert id == second_id
conn_three =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "False"
})
2020-05-12 19:59:26 +00:00
assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
refute id == third_id
# An activity that will expire:
# 2 hours
2020-08-22 17:46:01 +00:00
expires_in = 2 * 60 * 60
expires_at1 = DateTime.add(DateTime.utc_now(), expires_in)
conn_four =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
2020-05-12 19:59:26 +00:00
activity = Activity.get_by_id(fourth_id)
assert activity
{:ok, expires_at2, _} = DateTime.from_iso8601(activity.data["expires_at"])
assert Timex.compare(expires_at1, expires_at2, :minutes) == 0
2020-08-22 17:46:01 +00:00
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: fourth_id},
scheduled_at: expires_at2
2020-08-22 17:46:01 +00:00
)
end
test "automatically setting a post expiry if status_ttl_days is set" do
user = insert(:user, status_ttl_days: 1)
%{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "aa chikichiki banban"
})
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
activity = Activity.get_by_id_with_object(id)
{:ok, expires_at, _} = DateTime.from_iso8601(activity.data["expires_at"])
expiry_delay = Timex.diff(expires_at, DateTime.utc_now(), :hours)
assert(expiry_delay in [23, 24])
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
end
2020-02-12 15:43:07 +00:00
test "it fails to create a status if `expires_in` is less or equal than an hour", %{
conn: conn
} do
# 1 minute
expires_in = 1 * 60
2020-02-12 15:43:07 +00:00
assert %{"error" => "Expiry date is too soon"} =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
2020-02-12 15:43:07 +00:00
"status" => "oolong",
"expires_in" => expires_in
})
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(422)
2020-02-12 15:43:07 +00:00
# 5 minutes
expires_in = 5 * 60
2020-02-12 15:43:07 +00:00
assert %{"error" => "Expiry date is too soon"} =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
2020-02-12 15:43:07 +00:00
"status" => "oolong",
"expires_in" => expires_in
})
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(422)
2020-02-12 15:43:07 +00:00
end
test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "GNO/Linux"})
|> json_response_and_validate_schema(422)
end
test "posting an undefined status with an attachment", %{user: user, conn: conn} do
file = %Plug.Upload{
2020-10-13 15:37:24 +00:00
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)]
})
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(conn, 200)
end
Restrict media usage to owners In Mastodon media can only be used by owners and only be associated with a single post. We currently allow media to be associated with several posts and until now did not limit their usage in posts to media owners. However, media update and GET lookup was already limited to owners. (In accordance with allowing media reuse, we also still allow GET lookups of media already used in a post unlike Mastodon) Allowing reuse isn’t problematic per se, but allowing use by non-owners can be problematic if media ids of private-scoped posts can be guessed since creating a new post with this media id will reveal the uploaded file content and alt text. Given media ids are currently just part of a sequentieal series shared with some other objects, guessing media ids is with some persistence indeed feasible. E.g. sampline some public media ids from a real-world instance with 112 total and 61 monthly-active users: 17.465.096 at t0 17.472.673 at t1 = t0 + 4h 17.473.248 at t2 = t1 + 20min This gives about 30 new ids per minute of which most won't be local media but remote and local posts, poll answers etc. Assuming the default ratelimit of 15 post actions per 10s, scraping all media for the 4h interval takes about 84 minutes and scraping the 20min range mere 6.3 minutes. (Until the preceding commit, post updates were not rate limited at all, allowing even faster scraping.) If an attacker can infer (e.g. via reply to a follower-only post not accessbile to the attacker) some sensitive information was uploaded during a specific time interval and has some pointers regarding the nature of the information, identifying the specific upload out of all scraped media for this timerange is not impossible. Thus restrict media usage to owners. Checking ownership just in ActivitDraft would already be sufficient, since when a scheduled status actually gets posted it goes through ActivityDraft again, but would erroneously return a success status when scheduling an illegal post. Independently discovered and fixed by mint in Pleroma https://git.pleroma.social/pleroma/pleroma/-/commit/1afde067b12ad0062c1820091ea9b0a680819281
2024-04-24 15:46:18 +00:00
test "refuses to post non-owned media", %{conn: conn} do
other_user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: other_user.ap_id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "mew",
"media_ids" => [to_string(upload.id)]
})
assert json_response(conn, 422) == %{"error" => "forbidden"}
end
2023-01-02 02:41:56 +00:00
test "posting a status with an invalid language", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"language" => "invalid"
})
assert %{"error" => "Invalid language"} = json_response_and_validate_schema(conn, 422)
end
test "replying to a status", %{user: user, conn: conn} do
2020-05-12 19:59:26 +00:00
{:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
conn =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
2020-05-12 19:59:26 +00:00
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
activity = Activity.get_by_id(id)
assert activity.data["context"] == replied_to.data["context"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
end
test "replying to a direct message with visibility other than direct", %{
user: user,
conn: conn
} do
2020-05-12 19:59:26 +00:00
{:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
Enum.each(["public", "private", "unlisted"], fn visibility ->
conn =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "@#{user.nickname} hey",
"in_reply_to_id" => replied_to.id,
"visibility" => visibility
})
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "The message visibility must be direct"
}
end)
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
2020-05-12 19:59:26 +00:00
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a sensitive status", %{conn: conn} do
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a fake status", %{conn: conn} do
real_conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
})
2020-05-12 19:59:26 +00:00
real_status = json_response_and_validate_schema(real_conn, 200)
assert real_status
assert Object.get_by_ap_id(real_status["uri"])
real_status =
real_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "context"], nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
fake_conn =
2020-05-12 19:59:26 +00:00
conn
2021-01-22 20:37:49 +00:00
|> assign(:user, refresh_record(conn.assigns.user))
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
"preview" => true
})
2020-05-12 19:59:26 +00:00
fake_status = json_response_and_validate_schema(fake_conn, 200)
assert fake_status
refute Object.get_by_ap_id(fake_status["uri"])
fake_status =
fake_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "context"], nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
assert real_status == fake_status
end
test "posting a direct status", %{conn: conn} do
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"})
2020-05-12 19:59:26 +00:00
assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
assert response["visibility"] == "direct"
assert response["pleroma"]["direct_conversation_id"]
assert activity = Activity.get_by_id(id)
assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
assert activity.data["to"] == [user2.ap_id]
assert activity.data["cc"] == []
end
test "discloses application metadata when enabled" do
user = insert(:user, disclose_client: true)
%{user: _user, token: token, conn: conn} = oauth_access(["write:statuses"], user: user)
%Pleroma.Web.OAuth.Token{
app: %Pleroma.Web.OAuth.App{
2021-02-28 16:41:25 +00:00
client_name: app_name,
website: app_website
}
} = token
result =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe is my copilot"
})
assert %{
"content" => "cofe is my copilot"
} = json_response_and_validate_schema(result, 200)
activity = result.assigns.activity.id
result =
conn
|> get("/api/v1/statuses/#{activity}")
assert %{
"content" => "cofe is my copilot",
"application" => %{
2021-02-28 16:41:25 +00:00
"name" => ^app_name,
"website" => ^app_website
}
} = json_response_and_validate_schema(result, 200)
end
test "hides application metadata when disabled" do
user = insert(:user, disclose_client: false)
%{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
result =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "club mate is my wingman"
})
assert %{"content" => "club mate is my wingman"} =
json_response_and_validate_schema(result, 200)
activity = result.assigns.activity.id
result =
conn
|> get("/api/v1/statuses/#{activity}")
assert %{
"content" => "club mate is my wingman",
"application" => nil
} = json_response_and_validate_schema(result, 200)
end
end
describe "posting scheduled statuses" do
setup do: oauth_access(["write:statuses"])
test "creates a scheduled activity", %{conn: conn} do
2020-05-12 19:59:26 +00:00
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
2020-05-12 19:59:26 +00:00
assert %{"scheduled_at" => expected_scheduled_at} =
json_response_and_validate_schema(conn, 200)
assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
assert [] == Repo.all(Activity)
end
2021-02-11 10:01:48 +00:00
test "with expiration" do
%{conn: conn} = oauth_access(["write:statuses", "read:statuses"])
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
assert %{"id" => status_id, "params" => %{"expires_in" => 300}} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at,
"expires_in" => 300
})
|> json_response_and_validate_schema(200)
assert %{"id" => ^status_id, "params" => %{"expires_in" => 300}} =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/scheduled_statuses/#{status_id}")
|> json_response_and_validate_schema(200)
end
test "ignores nil values", %{conn: conn} do
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => nil
})
2020-05-12 19:59:26 +00:00
assert result = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(result["id"])
end
test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
2020-05-12 19:59:26 +00:00
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(120), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
file = %Plug.Upload{
2020-10-13 15:37:24 +00:00
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)],
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
2020-05-12 19:59:26 +00:00
assert %{"media_attachments" => [media_attachment]} =
json_response_and_validate_schema(conn, 200)
assert %{"type" => "image"} = media_attachment
end
Restrict media usage to owners In Mastodon media can only be used by owners and only be associated with a single post. We currently allow media to be associated with several posts and until now did not limit their usage in posts to media owners. However, media update and GET lookup was already limited to owners. (In accordance with allowing media reuse, we also still allow GET lookups of media already used in a post unlike Mastodon) Allowing reuse isn’t problematic per se, but allowing use by non-owners can be problematic if media ids of private-scoped posts can be guessed since creating a new post with this media id will reveal the uploaded file content and alt text. Given media ids are currently just part of a sequentieal series shared with some other objects, guessing media ids is with some persistence indeed feasible. E.g. sampline some public media ids from a real-world instance with 112 total and 61 monthly-active users: 17.465.096 at t0 17.472.673 at t1 = t0 + 4h 17.473.248 at t2 = t1 + 20min This gives about 30 new ids per minute of which most won't be local media but remote and local posts, poll answers etc. Assuming the default ratelimit of 15 post actions per 10s, scraping all media for the 4h interval takes about 84 minutes and scraping the 20min range mere 6.3 minutes. (Until the preceding commit, post updates were not rate limited at all, allowing even faster scraping.) If an attacker can infer (e.g. via reply to a follower-only post not accessbile to the attacker) some sensitive information was uploaded during a specific time interval and has some pointers regarding the nature of the information, identifying the specific upload out of all scraped media for this timerange is not impossible. Thus restrict media usage to owners. Checking ownership just in ActivitDraft would already be sufficient, since when a scheduled status actually gets posted it goes through ActivityDraft again, but would erroneously return a success status when scheduling an illegal post. Independently discovered and fixed by mint in Pleroma https://git.pleroma.social/pleroma/pleroma/-/commit/1afde067b12ad0062c1820091ea9b0a680819281
2024-04-24 15:46:18 +00:00
test "refuses to schedule post with non-owned media", %{conn: conn} do
other_user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: other_user.ap_id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "mew",
"scheduled_at" => DateTime.add(DateTime.utc_now(), 6, :minute),
"media_ids" => [to_string(upload.id)]
})
assert json_response(conn, 403) == %{"error" => "Access denied"}
end
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
%{conn: conn} do
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
2020-05-12 19:59:26 +00:00
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => scheduled_at
})
2020-05-12 19:59:26 +00:00
assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
assert [] == Repo.all(ScheduledActivity)
end
test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
2020-05-12 19:59:26 +00:00
# TODO
|> Kernel.<>("Z")
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
2020-05-12 19:59:26 +00:00
assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
end
test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
2020-05-12 19:59:26 +00:00
|> Kernel.<>("Z")
tomorrow =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.hours(36), :millisecond)
|> NaiveDateTime.to_iso8601()
2020-05-12 19:59:26 +00:00
|> Kernel.<>("Z")
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
2020-05-12 19:59:26 +00:00
assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
end
end
describe "posting polls" do
setup do: oauth_access(["write:statuses"])
test "posting a poll", %{conn: conn} do
time = NaiveDateTime.utc_now()
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "Who is the #bestgrill?",
2020-05-12 19:59:26 +00:00
"poll" => %{
"options" => ["Rei", "Asuka", "Misato"],
"expires_in" => 420
}
})
2020-05-12 19:59:26 +00:00
response = json_response_and_validate_schema(conn, 200)
assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
title in ["Rei", "Asuka", "Misato"]
end)
assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
2021-02-01 15:22:26 +00:00
assert response["poll"]["expired"] == false
question = Object.get_by_id(response["poll"]["id"])
# closed contains utc timezone
assert question.data["closed"] =~ "Z"
end
test "option limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_options])
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
2023-03-26 15:11:26 +00:00
"poll" => %{
"options" => Enum.map(0..limit, fn num -> "desu #{num}" end),
"expires_in" => 1
}
})
2020-05-12 19:59:26 +00:00
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll can't contain more than #{limit} options"
end
test "option character limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_option_chars])
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "...",
"poll" => %{
2023-03-26 15:11:26 +00:00
"options" => [String.duplicate(".", limit + 1), "lol"],
"expires_in" => 1
}
})
2020-05-12 19:59:26 +00:00
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll options cannot be longer than #{limit} characters each"
end
test "minimal date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :min_expiration])
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit - 1
}
})
2020-05-12 19:59:26 +00:00
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Expiration date is too soon"
end
test "maximum date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_expiration])
conn =
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit + 1
}
})
2020-05-12 19:59:26 +00:00
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Expiration date is too far in the future"
end
2021-02-01 15:22:26 +00:00
test "scheduled poll", %{conn: conn} do
clear_config([ScheduledActivity, :enabled], true)
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
%{"id" => scheduled_id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "very cool poll",
"poll" => %{
"options" => ~w(a b c),
"expires_in" => 420
},
"scheduled_at" => scheduled_at
})
|> json_response_and_validate_schema(200)
assert {:ok, %{id: activity_id}} =
2021-07-18 01:35:35 +00:00
perform_job(ScheduledActivityWorker, %{
2021-02-01 15:22:26 +00:00
activity_id: scheduled_id
})
2021-07-18 01:35:35 +00:00
refute_enqueued(worker: ScheduledActivityWorker)
2021-02-01 15:22:26 +00:00
object =
Activity
|> Repo.get(activity_id)
|> Object.normalize()
assert object.data["content"] == "very cool poll"
assert object.data["type"] == "Question"
assert length(object.data["oneOf"]) == 3
end
2023-03-26 03:20:07 +00:00
test "cannot have only one option", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => ["mew"], "expires_in" => 1}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll must contain at least 2 options"
end
test "cannot have only duplicated options", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => ["mew", "mew"], "expires_in" => 1}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll must contain at least 2 options"
end
end
test "get a status" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{activity.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
assert id == to_string(activity.id)
end
2020-03-20 10:04:37 +00:00
defp local_and_remote_activities do
local = insert(:note_activity)
remote = insert(:note_activity, local: false)
{:ok, local: local, remote: remote}
end
describe "status with restrict unauthenticated activities for local and remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
2020-03-20 10:04:37 +00:00
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
2020-03-20 10:04:37 +00:00
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(res_conn, :not_found) == %{
2020-03-20 10:04:37 +00:00
"error" => "Record not found"
}
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(res_conn, :not_found) == %{
2020-03-20 10:04:37 +00:00
"error" => "Record not found"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
end
end
describe "status with restrict unauthenticated activities for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
2020-03-20 10:04:37 +00:00
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(res_conn, :not_found) == %{
2020-03-20 10:04:37 +00:00
"error" => "Record not found"
}
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
end
end
describe "status with restrict unauthenticated activities for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
2020-03-20 10:04:37 +00:00
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(res_conn, :not_found) == %{
2020-03-20 10:04:37 +00:00
"error" => "Record not found"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
2020-05-12 19:59:26 +00:00
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
end
end
test "getting a status that doesn't exist returns 404" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
test "get a direct status" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} =
2020-05-12 19:59:26 +00:00
CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}")
[participation] = Participation.for_user(user)
2020-05-12 19:59:26 +00:00
res = json_response_and_validate_schema(conn, 200)
assert res["pleroma"]["direct_conversation_id"] == participation.id
end
test "get statuses by IDs" do
%{conn: conn} = oauth_access(["read:statuses"])
%{id: id1} = insert(:note_activity)
%{id: id2} = insert(:note_activity)
query_string = "ids[]=#{id1}&ids[]=#{id2}"
conn = get(conn, "/api/v1/statuses/?#{query_string}")
2020-05-12 19:59:26 +00:00
assert [%{"id" => ^id1}, %{"id" => ^id2}] =
Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
end
2020-03-20 10:04:37 +00:00
describe "getting statuses by ids with restricted unauthenticated for local and remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
2020-03-20 10:04:37 +00:00
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
2020-03-20 10:04:37 +00:00
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
2020-05-12 19:59:26 +00:00
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
2020-03-20 10:04:37 +00:00
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(res_conn, 200) == []
2020-03-20 10:04:37 +00:00
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
2020-05-12 19:59:26 +00:00
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
2020-03-20 10:04:37 +00:00
2020-05-12 19:59:26 +00:00
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
2020-03-20 10:04:37 +00:00
end
end
describe "getting statuses by ids with restricted unauthenticated for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
2020-03-20 10:04:37 +00:00
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
2020-05-12 19:59:26 +00:00
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
2020-03-20 10:04:37 +00:00
remote_id = remote.id
2020-05-12 19:59:26 +00:00
assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
2020-05-12 19:59:26 +00:00
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
2020-03-20 10:04:37 +00:00
2020-05-12 19:59:26 +00:00
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
2020-03-20 10:04:37 +00:00
end
end
describe "getting statuses by ids with restricted unauthenticated for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
2020-03-20 10:04:37 +00:00
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
2020-05-12 19:59:26 +00:00
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
2020-03-20 10:04:37 +00:00
local_id = local.id
2020-05-12 19:59:26 +00:00
assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
2020-03-20 10:04:37 +00:00
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
2020-05-12 19:59:26 +00:00
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
2020-03-20 10:04:37 +00:00
2020-05-12 19:59:26 +00:00
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
2020-03-20 10:04:37 +00:00
end
end
describe "deleting a status" do
test "when you created it" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
object = Object.normalize(activity, fetch: false)
content = object.data["content"]
source = object.data["source"]
result =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert match?(%{"content" => ^content, "text" => ^source}, result)
refute Activity.get_by_id(activity.id)
end
test "when it doesn't exist" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
conn =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
2020-05-12 19:59:26 +00:00
assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
end
test "when you didn't create it" do
%{conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity)
conn = delete(conn, "/api/v1/statuses/#{activity.id}")
assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
assert Activity.get_by_id(activity.id) == activity
end
test "when you're an admin or moderator", %{conn: conn} do
activity1 = insert(:note_activity)
activity2 = insert(:note_activity)
admin = insert(:user, is_admin: true)
moderator = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, admin)
|> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity1.id}")
2020-05-12 19:59:26 +00:00
assert %{} = json_response_and_validate_schema(res_conn, 200)
res_conn =
conn
|> assign(:user, moderator)
|> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity2.id}")
2020-05-12 19:59:26 +00:00
assert %{} = json_response_and_validate_schema(res_conn, 200)
refute Activity.get_by_id(activity1.id)
refute Activity.get_by_id(activity2.id)
end
2023-05-25 22:40:38 +00:00
test "when you're privileged and the user is banned", %{conn: conn} do
clear_config([:instance, :moderator_privileges], [:messages_delete])
posting_user = insert(:user, is_active: false)
refute posting_user.is_active
activity = insert(:note_activity, user: posting_user)
user = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity.id}")
assert %{} = json_response_and_validate_schema(res_conn, 200)
# assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() ==
# "@#{user.nickname} deleted status ##{activity.id}"
refute Activity.get_by_id(activity.id)
end
end
describe "reblogging" do
setup do: oauth_access(["write:statuses"])
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true
2020-05-12 19:59:26 +00:00
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
activity = insert(:note_activity)
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
2020-05-12 19:59:26 +00:00
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
end
test "reblogs privately and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/statuses/#{activity.id}/reblog",
%{"visibility" => "private"}
)
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
2020-05-12 19:59:26 +00:00
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "reblogged status for another user" do
activity = insert(:note_activity)
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, _} = CommonAPI.favorite(user2, activity.id)
{:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
2020-05-21 11:16:21 +00:00
{:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
{:ok, _} = CommonAPI.repeat(activity.id, user2)
conn_res =
build_conn()
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
2020-10-15 12:54:59 +00:00
"reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
"reblogged" => false,
"favourited" => false,
"bookmarked" => false
2020-05-12 19:59:26 +00:00
} = json_response_and_validate_schema(conn_res, 200)
conn_res =
build_conn()
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
"reblogged" => true,
"favourited" => true,
"bookmarked" => true
2020-05-12 19:59:26 +00:00
} = json_response_and_validate_schema(conn_res, 200)
assert to_string(activity.id) == id
end
test "author can reblog own private status", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
end
describe "unreblogging" do
setup do: oauth_access(["write:statuses"])
test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
activity = insert(:note_activity)
2020-05-21 11:16:21 +00:00
{:ok, _} = CommonAPI.repeat(activity.id, user)
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unreblog")
2020-05-12 19:59:26 +00:00
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 error when activity does not exist", %{conn: conn} do
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/foo/unreblog")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "favoriting" do
setup do: oauth_access(["write:favourites"])
test "favs a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
2020-05-12 19:59:26 +00:00
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "favoriting twice will just return 200", %{conn: conn} do
activity = insert(:note_activity)
2020-05-12 19:59:26 +00:00
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
2020-05-12 19:59:26 +00:00
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
|> json_response_and_validate_schema(200)
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "unfavoriting" do
setup do: oauth_access(["write:favourites"])
test "unfavorites a status and returns it", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _} = CommonAPI.favorite(user, activity.id)
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
2020-05-12 19:59:26 +00:00
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 error for a wrong id", %{conn: conn} do
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unfavourite")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "pinned statuses" do
setup do: oauth_access(["write:accounts"])
setup %{user: user} do
2020-05-12 19:59:26 +00:00
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
%{activity: activity}
end
setup do: clear_config([:instance, :max_pinned_statuses], 1)
test "pin status", %{conn: conn, user: user, activity: activity} do
id = activity.id
assert %{"id" => ^id, "pinned" => true} =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
end
test "non authenticated user", %{activity: activity} do
assert build_conn()
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(403) == %{"error" => "Invalid credentials."}
end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
2020-05-12 19:59:26 +00:00
{:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{dm.id}/pin")
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "Non-public status cannot be pinned"
}
end
test "pin by another user", %{activity: activity} do
%{conn: conn} = oauth_access(["write:accounts"])
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(422) == %{"error" => "Someone else's status cannot be pinned"}
end
test "unpin status", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "pinned" => false} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unpin")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
assert [] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
end
test "/unpin: returns 404 error when activity doesn't exist", %{conn: conn} do
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unpin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
2020-05-12 19:59:26 +00:00
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
id_str_one = to_string(activity_one.id)
assert %{"id" => ^id_str_one, "pinned" => true} =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id_str_one}/pin")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
user = refresh_record(user)
assert %{"error" => "You have already pinned the maximum number of statuses"} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity_two.id}/pin")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(400)
end
test "on pin removes deletion job, on unpin reschedule deletion" do
%{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
expires_in = 2 * 60 * 60
expires_at1 = DateTime.add(DateTime.utc_now(), expires_in)
assert %{"id" => id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(200)
activity = Activity.get_by_id(id)
{:ok, expires_at2, _} = DateTime.from_iso8601(activity.data["expires_at"])
assert Timex.compare(expires_at1, expires_at2, :minutes) == 0
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at2
)
assert %{"id" => ^id, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id}/pin")
|> json_response_and_validate_schema(200)
refute_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at2
)
assert %{"id" => ^id, "pinned" => false} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id}/unpin")
|> json_response_and_validate_schema(200)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at2
)
end
end
describe "cards" do
setup do
Pleroma.StaticStubbedConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
oauth_access(["read:statuses"])
end
test "returns rich-media card", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"})
card_data = %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"title" => "The Rock",
"type" => "link",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
}
}
}
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(200)
assert response == card_data
# works with private posts
{:ok, activity} =
CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"})
response_two =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(200)
assert response_two == card_data
end
end
test "bookmarks" do
bookmarks_uri = "/api/v1/bookmarks"
%{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
author = insert(:user)
2020-05-12 19:59:26 +00:00
{:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
{:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
2020-05-12 19:59:26 +00:00
response1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity1.id}/bookmark")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
2020-05-12 19:59:26 +00:00
response2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity2.id}/bookmark")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
bookmarks = get(conn, bookmarks_uri)
2020-05-12 19:59:26 +00:00
assert [
json_response_and_validate_schema(response2, 200),
json_response_and_validate_schema(response1, 200)
] ==
json_response_and_validate_schema(bookmarks, 200)
2020-05-12 19:59:26 +00:00
response1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity1.id}/unbookmark")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
bookmarks = get(conn, bookmarks_uri)
2020-05-12 19:59:26 +00:00
assert [json_response_and_validate_schema(response2, 200)] ==
json_response_and_validate_schema(bookmarks, 200)
end
describe "conversation muting" do
setup do: oauth_access(["write:mutes"])
setup do
post_user = insert(:user)
2020-05-12 19:59:26 +00:00
{:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
%{activity: activity}
end
test "mute conversation", %{conn: conn, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => true} =
conn
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
end
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
2020-05-12 19:59:26 +00:00
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
2020-05-12 19:59:26 +00:00
assert json_response_and_validate_schema(conn, 400) == %{
"error" => "conversation is already muted"
}
end
test "unmute conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => false} =
conn
# |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unmute")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
end
end
test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
2020-05-12 19:59:26 +00:00
{:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
# Reply to status from another user
conn1 =
conn
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
2020-05-12 19:59:26 +00:00
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
activity = Activity.get_by_id_with_object(id)
assert Object.normalize(activity, fetch: false).data["inReplyTo"] ==
Object.normalize(replied_to, fetch: false).data["id"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
# Reblog from the third user
conn2 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
2020-05-12 19:59:26 +00:00
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
2020-05-12 19:59:26 +00:00
json_response_and_validate_schema(conn2, 200)
assert to_string(activity.id) == id
# Getting third user status
conn3 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("/api/v1/timelines/home")
2021-02-16 23:45:01 +00:00
[reblogged_activity] = json_response_and_validate_schema(conn3, 200)
assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
end
describe "GET /api/v1/statuses/:id/favourited_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
2020-05-12 19:59:26 +00:00
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
%{activity: activity}
end
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been favorited yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have favorited the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
2020-05-12 19:59:26 +00:00
status: "@#{other_user.nickname} wanna get some #cofe together?",
visibility: "direct"
})
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
build_conn()
|> get(favourited_by_url)
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(404)
conn =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
conn
|> assign(:token, nil)
|> get(favourited_by_url)
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(404)
response =
conn
|> get(favourited_by_url)
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
clear_config([:instance, :show_reactions], false)
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
end
describe "GET /api/v1/statuses/:id/reblogged_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
2020-05-12 19:59:26 +00:00
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
%{activity: activity}
end
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
2020-05-21 11:16:21 +00:00
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been reblogged yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
2020-05-21 11:16:21 +00:00
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status privately", %{
2020-05-21 11:16:21 +00:00
conn: conn
} do
other_user = insert(:user)
2020-05-21 11:16:21 +00:00
{:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
2020-05-21 11:16:21 +00:00
{:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
2020-05-21 11:16:21 +00:00
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
2020-05-12 19:59:26 +00:00
status: "@#{other_user.nickname} wanna get some #cofe together?",
visibility: "direct"
})
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(404)
response =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(200)
assert [] == response
end
end
test "context" do
user = insert(:user)
2020-05-12 19:59:26 +00:00
{:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
{:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
{:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
{:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
{:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
response =
build_conn()
|> get("/api/v1/statuses/#{id3}/context")
2020-05-12 19:59:26 +00:00
|> json_response_and_validate_schema(:ok)
assert %{
"ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
"descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
} = response
end
test "context when restrict_unauthenticated is on" do
user = insert(:user)
remote_user = insert(:user, local: false)
{:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
{:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
{:ok, %{id: id3}} =
CommonAPI.post(remote_user, %{status: "3", in_reply_to_status_id: id2, local: false})
response =
build_conn()
|> get("/api/v1/statuses/#{id2}/context")
|> json_response_and_validate_schema(:ok)
assert %{
"ancestors" => [%{"id" => ^id1}],
"descendants" => [%{"id" => ^id3}]
} = response
clear_config([:restrict_unauthenticated, :activities, :local], true)
response =
build_conn()
|> get("/api/v1/statuses/#{id2}/context")
|> json_response_and_validate_schema(:ok)
assert %{
"ancestors" => [],
"descendants" => []
} = response
end
test "favorites paginate correctly" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
{:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
{:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
result =
conn
|> get("/api/v1/favourites?limit=1")
assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
assert post_id == second_post.id
# Using the header for pagination works correctly
[next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
[_, max_id] = Regex.run(~r/max_id=([^&>]+)/, next)
assert max_id == third_favorite.id
result =
conn
|> get("/api/v1/favourites?max_id=#{max_id}")
assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
json_response_and_validate_schema(result, 200)
assert first_post_id == first_post.id
assert third_post_id == third_post.id
end
test "returns the favorites of a user" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
2020-05-12 19:59:26 +00:00
{:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
{:ok, last_like} = CommonAPI.favorite(user, activity.id)
first_conn = get(conn, "/api/v1/favourites")
2020-05-12 19:59:26 +00:00
assert [status] = json_response_and_validate_schema(first_conn, 200)
assert status["id"] == to_string(activity.id)
assert [{"link", _link_header}] =
Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
# Honours query params
{:ok, second_activity} =
CommonAPI.post(other_user, %{
2020-05-12 19:59:26 +00:00
status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
{:ok, _} = CommonAPI.favorite(user, second_activity.id)
second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
2020-05-12 19:59:26 +00:00
assert [second_status] = json_response_and_validate_schema(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
third_conn = get(conn, "/api/v1/favourites?limit=0")
2020-05-12 19:59:26 +00:00
assert [] = json_response_and_validate_schema(third_conn, 200)
end
2020-02-18 13:09:50 +00:00
test "expires_at is nil for another user" do
%{conn: conn, user: user} = oauth_access(["read:statuses"])
2020-08-22 17:46:01 +00:00
expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
2020-05-12 19:59:26 +00:00
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
2020-02-18 13:09:50 +00:00
2020-08-22 17:46:01 +00:00
assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
2020-05-12 19:59:26 +00:00
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
2020-02-18 13:09:50 +00:00
2020-08-22 17:46:01 +00:00
{:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
assert Timex.compare(expires_at, a_expires_at, :minutes) == 0
2020-08-22 17:46:01 +00:00
2020-02-18 13:09:50 +00:00
%{conn: conn} = oauth_access(["read:statuses"])
assert %{"pleroma" => %{"expires_at" => nil}} =
2020-05-12 19:59:26 +00:00
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
2020-02-18 13:09:50 +00:00
end
describe "local-only statuses" do
test "posting a local only status" do
%{user: _user, conn: conn} = oauth_access(["write:statuses"])
2020-10-02 17:00:50 +00:00
conn_one =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"visibility" => "local"
})
2020-10-02 17:00:50 +00:00
local = Utils.as_local_public()
2020-10-02 17:00:50 +00:00
assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
json_response_and_validate_schema(conn_one, 200)
assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
end
2020-10-02 17:00:50 +00:00
test "other users can read local-only posts" do
user = insert(:user)
%{user: _reader, conn: conn} = oauth_access(["read:statuses"])
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
received =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
assert received["id"] == activity.id
end
test "anonymous users cannot see local-only posts" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
_received =
build_conn()
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:not_found)
end
2020-10-02 17:00:50 +00:00
end
describe "muted reactions" do
test "index" do
%{conn: conn, user: user} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
deactivated_user = insert(:user)
{:ok, _} = CommonAPI.react_with_emoji(activity.id, deactivated_user, "🎅")
User.set_activation(deactivated_user, false)
result =
conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}")
|> json_response_and_validate_schema(200)
assert [
%{
"emoji_reactions" => [],
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
test "show" do
# %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
%{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert %{
"pleroma" => %{
"emoji_reactions" => []
}
} = result
result =
conn
|> get("/api/v1/statuses/#{activity.id}?with_muted=true")
|> json_response_and_validate_schema(200)
assert %{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
} = result
end
end
describe "posting quotes" do
setup do: oauth_access(["write:statuses"])
test "posting a quote", %{conn: conn} do
user = insert(:user)
{:ok, quoted_status} = CommonAPI.post(user, %{status: "tell me, for whom do you fight?"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "Hmph, how very glib",
"quote_id" => quoted_status.id
})
response = json_response_and_validate_schema(conn, 200)
assert response["quote_id"] == quoted_status.id
assert response["quote"]["id"] == quoted_status.id
assert response["quote"]["content"] == quoted_status.object.data["content"]
assert response["pleroma"]["context"] == quoted_status.data["context"]
end
test "posting a quote, quoting a status that isn't public", %{conn: conn} do
user = insert(:user)
Enum.each(["private", "local", "direct"], fn visibility ->
{:ok, quoted_status} =
CommonAPI.post(user, %{
status: "tell me, for whom do you fight?",
visibility: visibility
})
assert %{"error" => "You can only quote public or unlisted statuses"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "Hmph, how very glib",
"quote_id" => quoted_status.id
})
|> json_response_and_validate_schema(422)
end)
end
test "posting a quote, after quote, the status gets deleted", %{conn: conn} do
user = insert(:user)
{:ok, quoted_status} =
CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
resp =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "I fight for eorzea!",
"quote_id" => quoted_status.id
})
|> json_response_and_validate_schema(200)
{:ok, _} = CommonAPI.delete(quoted_status.id, user)
resp =
conn
|> get("/api/v1/statuses/#{resp["id"]}")
|> json_response_and_validate_schema(200)
assert is_nil(resp["quote"])
end
test "posting a quote of a deleted status", %{conn: conn} do
user = insert(:user)
{:ok, quoted_status} =
CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
{:ok, _} = CommonAPI.delete(quoted_status.id, user)
assert %{"error" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "I fight for eorzea!",
"quote_id" => quoted_status.id
})
|> json_response_and_validate_schema(422)
end
test "posting a quote of a status that doesn't exist", %{conn: conn} do
assert %{"error" => "You can't quote a status that doesn't exist"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "I fight for eorzea!",
"quote_id" => "oops"
})
|> json_response_and_validate_schema(422)
end
end
describe "get status history" do
setup do
%{conn: build_conn()}
end
test "unedited post", %{conn: conn} do
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
assert [_] = json_response_and_validate_schema(conn, 200)
end
test "edited post", %{conn: conn} do
note =
insert(
:note,
data: %{
"formerRepresentations" => %{
"type" => "OrderedCollection",
"orderedItems" => [
%{
"type" => "Note",
"content" => "mew mew 2",
"summary" => "title 2"
},
%{
"type" => "Note",
"content" => "mew mew 1",
"summary" => "title 1"
}
],
"totalItems" => 2
}
}
)
activity = insert(:note_activity, note: note)
conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
assert [%{"spoiler_text" => "title 1"}, %{"spoiler_text" => "title 2"}, _] =
json_response_and_validate_schema(conn, 200)
end
end
describe "translating statuses" do
setup do
clear_config([:translator, :enabled], true)
clear_config([:translator, :module], Pleroma.Akkoma.Translators.DeepL)
clear_config([:deepl, :api_key], "deepl_api_key")
oauth_access(["read:statuses"])
end
test "listing languages", %{conn: conn} do
Tesla.Mock.mock_global(fn
%{method: :get, url: "https://api-free.deepl.com/v2/languages?type=source"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!([
%{language: "en", name: "English"}
])
}
%{method: :get, url: "https://api-free.deepl.com/v2/languages?type=target"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!([
%{language: "ja", name: "Japanese"}
])
}
end)
conn =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/akkoma/translation/languages")
response = json_response_and_validate_schema(conn, 200)
assert %{
"source" => [%{"code" => "en", "name" => "English"}],
"target" => [%{"code" => "ja", "name" => "Japanese"}]
} = response
end
test "should return text and detected language", %{conn: conn} do
clear_config([:deepl, :tier], :free)
Tesla.Mock.mock_global(fn
%{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!(%{
translations: [
%{
"text" => "Tell me, for whom do you fight?",
"detected_source_language" => "ja"
}
]
})
}
end)
user = insert(:user)
{:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/statuses/#{to_translate.id}/translations/en")
response = json_response_and_validate_schema(conn, 200)
assert response["text"] == "Tell me, for whom do you fight?"
assert response["detected_language"] == "ja"
end
test "should not allow translating of statuses you cannot see", %{conn: conn} do
clear_config([:deepl, :tier], :free)
Tesla.Mock.mock_global(fn
%{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!(%{
translations: [
%{
"text" => "Tell me, for whom do you fight?",
"detected_source_language" => "ja"
}
]
})
}
end)
user = insert(:user)
{:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?", visibility: "private"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/statuses/#{to_translate.id}/translations/en")
json_response_and_validate_schema(conn, 404)
end
end
describe "get status source" do
setup do
%{conn: build_conn()}
end
test "it returns the source", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn = get(conn, "/api/v1/statuses/#{activity.id}/source")
id = activity.id
assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
json_response_and_validate_schema(conn, 200)
end
end
describe "update status" do
setup do
oauth_access(["write:statuses"])
end
test "it updates the status" do
%{conn: conn, user: user} = oauth_access(["write:statuses", "read:statuses"])
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(200)
assert response["content"] == "edited"
assert response["spoiler_text"] == "lol"
response =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert response["content"] == "edited"
assert response["spoiler_text"] == "lol"
end
test "it updates the attachments", %{conn: conn, user: user} do
attachment = insert(:attachment, user: user)
attachment_id = to_string(attachment.id)
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "mew mew #abc",
"spoiler_text" => "#def",
"media_ids" => [attachment_id]
})
|> json_response_and_validate_schema(200)
assert [%{"id" => ^attachment_id}] = response["media_attachments"]
end
Restrict media usage to owners In Mastodon media can only be used by owners and only be associated with a single post. We currently allow media to be associated with several posts and until now did not limit their usage in posts to media owners. However, media update and GET lookup was already limited to owners. (In accordance with allowing media reuse, we also still allow GET lookups of media already used in a post unlike Mastodon) Allowing reuse isn’t problematic per se, but allowing use by non-owners can be problematic if media ids of private-scoped posts can be guessed since creating a new post with this media id will reveal the uploaded file content and alt text. Given media ids are currently just part of a sequentieal series shared with some other objects, guessing media ids is with some persistence indeed feasible. E.g. sampline some public media ids from a real-world instance with 112 total and 61 monthly-active users: 17.465.096 at t0 17.472.673 at t1 = t0 + 4h 17.473.248 at t2 = t1 + 20min This gives about 30 new ids per minute of which most won't be local media but remote and local posts, poll answers etc. Assuming the default ratelimit of 15 post actions per 10s, scraping all media for the 4h interval takes about 84 minutes and scraping the 20min range mere 6.3 minutes. (Until the preceding commit, post updates were not rate limited at all, allowing even faster scraping.) If an attacker can infer (e.g. via reply to a follower-only post not accessbile to the attacker) some sensitive information was uploaded during a specific time interval and has some pointers regarding the nature of the information, identifying the specific upload out of all scraped media for this timerange is not impossible. Thus restrict media usage to owners. Checking ownership just in ActivitDraft would already be sufficient, since when a scheduled status actually gets posted it goes through ActivityDraft again, but would erroneously return a success status when scheduling an illegal post. Independently discovered and fixed by mint in Pleroma https://git.pleroma.social/pleroma/pleroma/-/commit/1afde067b12ad0062c1820091ea9b0a680819281
2024-04-24 15:46:18 +00:00
test "it does not update to non-owned attachments", %{conn: conn, user: user} do
other_user = insert(:user)
attachment = insert(:attachment, user: other_user)
attachment_id = to_string(attachment.id)
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "mew mew #abc",
"spoiler_text" => "#def",
"media_ids" => [attachment_id]
})
assert json_response(conn, 400) == %{"error" => "internal_server_error"}
end
test "it does not update visibility", %{conn: conn, user: user} do
{:ok, activity} =
CommonAPI.post(user, %{
status: "mew mew #abc",
spoiler_text: "#def",
visibility: "private"
})
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(200)
assert response["visibility"] == "private"
end
test "it refuses to update when original post is not by the user", %{conn: conn} do
another_user = insert(:user)
{:ok, activity} =
CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(:forbidden)
end
test "it returns 404 if the user cannot see the post", %{conn: conn} do
another_user = insert(:user)
{:ok, activity} =
CommonAPI.post(another_user, %{
status: "mew mew #abc",
spoiler_text: "#def",
visibility: "private"
})
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(:not_found)
end
end
end