1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
6 use Pleroma.Web.ConnCase
7 use Oban.Testing, repo: Pleroma.Repo
10 alias Pleroma.Conversation.Participation
13 alias Pleroma.ScheduledActivity
14 alias Pleroma.Tests.ObanHelpers
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Workers.ScheduledActivityWorker
21 import Pleroma.Factory
23 setup do: clear_config([:instance, :federating])
24 setup do: clear_config([:instance, :allow_relay])
25 setup do: clear_config([:rich_media, :enabled])
26 setup do: clear_config([:mrf, :policies])
27 setup do: clear_config([:mrf_keyword, :reject])
28 setup do: clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
29 setup do: clear_config([Pleroma.Uploaders.Local, :uploads], "uploads")
31 describe "posting statuses" do
32 setup do: oauth_access(["write:statuses"])
34 test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
35 clear_config([:instance, :federating], true)
36 Config.get([:instance, :allow_relay], true)
40 |> put_req_header("content-type", "application/json")
41 |> post("/api/v1/statuses", %{
42 "content_type" => "text/plain",
43 "source" => "Pleroma FE",
44 "status" => "Hello world",
45 "visibility" => "public"
47 |> json_response_and_validate_schema(200)
49 assert response["reblogs_count"] == 0
50 ObanHelpers.perform_all()
54 |> get("/api/v1/statuses/#{response["id"]}", %{})
55 |> json_response_and_validate_schema(200)
57 assert response["reblogs_count"] == 0
60 test "posting a status", %{conn: conn} do
61 idempotency_key = "Pikachu rocks!"
65 |> put_req_header("content-type", "application/json")
66 |> put_req_header("idempotency-key", idempotency_key)
67 |> post("/api/v1/statuses", %{
69 "spoiler_text" => "2hu",
77 "spoiler_text" => "2hu",
80 } = json_response_and_validate_schema(conn_one, 200)
82 assert Activity.get_by_id(id)
86 |> put_req_header("content-type", "application/json")
87 |> put_req_header("idempotency-key", idempotency_key)
88 |> post("/api/v1/statuses", %{
90 "spoiler_text" => "2hu",
94 # Idempotency plug response means detection fail
95 assert %{"id" => second_id} = json_response(conn_two, 200)
96 assert id == second_id
100 |> put_req_header("content-type", "application/json")
101 |> post("/api/v1/statuses", %{
103 "spoiler_text" => "2hu",
104 "sensitive" => "False"
107 assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
108 refute id == third_id
110 # An activity that will expire:
112 expires_in = 2 * 60 * 60
114 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
118 |> put_req_header("content-type", "application/json")
119 |> post("/api/v1/statuses", %{
120 "status" => "oolong",
121 "expires_in" => expires_in
124 assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
126 assert Activity.get_by_id(fourth_id)
129 worker: Pleroma.Workers.PurgeExpiredActivity,
130 args: %{activity_id: fourth_id},
131 scheduled_at: expires_at
135 test "automatically setting a post expiry if status_ttl_days is set" do
136 user = insert(:user, status_ttl_days: 1)
137 %{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
141 |> put_req_header("content-type", "application/json")
142 |> post("/api/v1/statuses", %{
143 "status" => "aa chikichiki banban"
146 assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
148 activity = Activity.get_by_id_with_object(id)
149 {:ok, expires_at, _} = DateTime.from_iso8601(activity.data["expires_at"])
158 worker: Pleroma.Workers.PurgeExpiredActivity,
159 args: %{activity_id: id},
160 scheduled_at: DateTime.add(DateTime.utc_now(), 1 * 60 * 60 * 24)
164 test "it fails to create a status if `expires_in` is less or equal than an hour", %{
170 assert %{"error" => "Expiry date is too soon"} =
172 |> put_req_header("content-type", "application/json")
173 |> post("/api/v1/statuses", %{
174 "status" => "oolong",
175 "expires_in" => expires_in
177 |> json_response_and_validate_schema(422)
182 assert %{"error" => "Expiry date is too soon"} =
184 |> put_req_header("content-type", "application/json")
185 |> post("/api/v1/statuses", %{
186 "status" => "oolong",
187 "expires_in" => expires_in
189 |> json_response_and_validate_schema(422)
192 test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
193 clear_config([:mrf_keyword, :reject], ["GNO"])
194 clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
196 assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
198 |> put_req_header("content-type", "application/json")
199 |> post("/api/v1/statuses", %{"status" => "GNO/Linux"})
200 |> json_response_and_validate_schema(422)
203 test "posting an undefined status with an attachment", %{user: user, conn: conn} do
205 content_type: "image/jpeg",
206 path: Path.absname("test/fixtures/image.jpg"),
207 filename: "an_image.jpg"
210 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
214 |> put_req_header("content-type", "application/json")
215 |> post("/api/v1/statuses", %{
216 "media_ids" => [to_string(upload.id)]
219 assert json_response_and_validate_schema(conn, 200)
222 test "replying to a status", %{user: user, conn: conn} do
223 {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
227 |> put_req_header("content-type", "application/json")
228 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
230 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
232 activity = Activity.get_by_id(id)
234 assert activity.data["context"] == replied_to.data["context"]
235 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
238 test "replying to a direct message with visibility other than direct", %{
242 {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
244 Enum.each(["public", "private", "unlisted"], fn visibility ->
247 |> put_req_header("content-type", "application/json")
248 |> post("/api/v1/statuses", %{
249 "status" => "@#{user.nickname} hey",
250 "in_reply_to_id" => replied_to.id,
251 "visibility" => visibility
254 assert json_response_and_validate_schema(conn, 422) == %{
255 "error" => "The message visibility must be direct"
260 test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
263 |> put_req_header("content-type", "application/json")
264 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
266 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
267 assert Activity.get_by_id(id)
270 test "posting a sensitive status", %{conn: conn} do
273 |> put_req_header("content-type", "application/json")
274 |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
276 assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
277 json_response_and_validate_schema(conn, 200)
279 assert Activity.get_by_id(id)
282 test "posting a fake status", %{conn: conn} do
285 |> put_req_header("content-type", "application/json")
286 |> post("/api/v1/statuses", %{
288 "\"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"
291 real_status = json_response_and_validate_schema(real_conn, 200)
294 assert Object.get_by_ap_id(real_status["uri"])
298 |> Map.put("id", nil)
299 |> Map.put("url", nil)
300 |> Map.put("uri", nil)
301 |> Map.put("created_at", nil)
302 |> Kernel.put_in(["pleroma", "context"], nil)
303 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
307 |> assign(:user, refresh_record(conn.assigns.user))
308 |> put_req_header("content-type", "application/json")
309 |> post("/api/v1/statuses", %{
311 "\"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",
315 fake_status = json_response_and_validate_schema(fake_conn, 200)
318 refute Object.get_by_ap_id(fake_status["uri"])
322 |> Map.put("id", nil)
323 |> Map.put("url", nil)
324 |> Map.put("uri", nil)
325 |> Map.put("created_at", nil)
326 |> Kernel.put_in(["pleroma", "context"], nil)
327 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
329 assert real_status == fake_status
332 test "fake statuses' preview card is not cached", %{conn: conn} do
333 clear_config([:rich_media, :enabled], true)
335 Tesla.Mock.mock_global(fn
338 url: "https://example.com/twitter-card"
340 %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
343 apply(HttpRequestMock, :request, [env])
348 |> put_req_header("content-type", "application/json")
349 |> post("/api/v1/statuses", %{
350 "status" => "https://example.com/ogp",
356 |> put_req_header("content-type", "application/json")
357 |> post("/api/v1/statuses", %{
358 "status" => "https://example.com/twitter-card",
362 assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200)
364 assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} =
365 json_response_and_validate_schema(conn2, 200)
368 test "posting a status with OGP link preview", %{conn: conn} do
369 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
370 clear_config([:rich_media, :enabled], true)
374 |> put_req_header("content-type", "application/json")
375 |> post("/api/v1/statuses", %{
376 "status" => "https://example.com/ogp"
379 assert %{"id" => id, "card" => %{"title" => "The Rock"}} =
380 json_response_and_validate_schema(conn, 200)
382 assert Activity.get_by_id(id)
385 test "posting a direct status", %{conn: conn} do
386 user2 = insert(:user)
387 content = "direct cofe @#{user2.nickname}"
391 |> put_req_header("content-type", "application/json")
392 |> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"})
394 assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
395 assert response["visibility"] == "direct"
396 assert response["pleroma"]["direct_conversation_id"]
397 assert activity = Activity.get_by_id(id)
398 assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
399 assert activity.data["to"] == [user2.ap_id]
400 assert activity.data["cc"] == []
403 test "discloses application metadata when enabled" do
404 user = insert(:user, disclose_client: true)
405 %{user: _user, token: token, conn: conn} = oauth_access(["write:statuses"], user: user)
407 %Pleroma.Web.OAuth.Token{
408 app: %Pleroma.Web.OAuth.App{
409 client_name: app_name,
416 |> put_req_header("content-type", "application/json")
417 |> post("/api/v1/statuses", %{
418 "status" => "cofe is my copilot"
422 "content" => "cofe is my copilot"
423 } = json_response_and_validate_schema(result, 200)
425 activity = result.assigns.activity.id
429 |> get("/api/v1/statuses/#{activity}")
432 "content" => "cofe is my copilot",
435 "website" => ^app_website
437 } = json_response_and_validate_schema(result, 200)
440 test "hides application metadata when disabled" do
441 user = insert(:user, disclose_client: false)
442 %{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
446 |> put_req_header("content-type", "application/json")
447 |> post("/api/v1/statuses", %{
448 "status" => "club mate is my wingman"
451 assert %{"content" => "club mate is my wingman"} =
452 json_response_and_validate_schema(result, 200)
454 activity = result.assigns.activity.id
458 |> get("/api/v1/statuses/#{activity}")
461 "content" => "club mate is my wingman",
463 } = json_response_and_validate_schema(result, 200)
467 describe "posting scheduled statuses" do
468 setup do: oauth_access(["write:statuses"])
470 test "creates a scheduled activity", %{conn: conn} do
472 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
473 |> NaiveDateTime.to_iso8601()
478 |> put_req_header("content-type", "application/json")
479 |> post("/api/v1/statuses", %{
480 "status" => "scheduled",
481 "scheduled_at" => scheduled_at
484 assert %{"scheduled_at" => expected_scheduled_at} =
485 json_response_and_validate_schema(conn, 200)
487 assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
488 assert [] == Repo.all(Activity)
491 test "with expiration" do
492 %{conn: conn} = oauth_access(["write:statuses", "read:statuses"])
495 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
496 |> NaiveDateTime.to_iso8601()
499 assert %{"id" => status_id, "params" => %{"expires_in" => 300}} =
501 |> put_req_header("content-type", "application/json")
502 |> post("/api/v1/statuses", %{
503 "status" => "scheduled",
504 "scheduled_at" => scheduled_at,
507 |> json_response_and_validate_schema(200)
509 assert %{"id" => ^status_id, "params" => %{"expires_in" => 300}} =
511 |> put_req_header("content-type", "application/json")
512 |> get("/api/v1/scheduled_statuses/#{status_id}")
513 |> json_response_and_validate_schema(200)
516 test "ignores nil values", %{conn: conn} do
519 |> put_req_header("content-type", "application/json")
520 |> post("/api/v1/statuses", %{
521 "status" => "not scheduled",
522 "scheduled_at" => nil
525 assert result = json_response_and_validate_schema(conn, 200)
526 assert Activity.get_by_id(result["id"])
529 test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
531 NaiveDateTime.utc_now()
532 |> NaiveDateTime.add(:timer.minutes(120), :millisecond)
533 |> NaiveDateTime.to_iso8601()
537 content_type: "image/jpeg",
538 path: Path.absname("test/fixtures/image.jpg"),
539 filename: "an_image.jpg"
542 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
546 |> put_req_header("content-type", "application/json")
547 |> post("/api/v1/statuses", %{
548 "media_ids" => [to_string(upload.id)],
549 "status" => "scheduled",
550 "scheduled_at" => scheduled_at
553 assert %{"media_attachments" => [media_attachment]} =
554 json_response_and_validate_schema(conn, 200)
556 assert %{"type" => "image"} = media_attachment
559 test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
562 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
563 |> NaiveDateTime.to_iso8601()
568 |> put_req_header("content-type", "application/json")
569 |> post("/api/v1/statuses", %{
570 "status" => "not scheduled",
571 "scheduled_at" => scheduled_at
574 assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
575 assert [] == Repo.all(ScheduledActivity)
578 test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
580 NaiveDateTime.utc_now()
581 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
582 |> NaiveDateTime.to_iso8601()
586 attrs = %{params: %{}, scheduled_at: today}
587 {:ok, _} = ScheduledActivity.create(user, attrs)
588 {:ok, _} = ScheduledActivity.create(user, attrs)
592 |> put_req_header("content-type", "application/json")
593 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
595 assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
598 test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
600 NaiveDateTime.utc_now()
601 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
602 |> NaiveDateTime.to_iso8601()
606 NaiveDateTime.utc_now()
607 |> NaiveDateTime.add(:timer.hours(36), :millisecond)
608 |> NaiveDateTime.to_iso8601()
611 attrs = %{params: %{}, scheduled_at: today}
612 {:ok, _} = ScheduledActivity.create(user, attrs)
613 {:ok, _} = ScheduledActivity.create(user, attrs)
614 {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
618 |> put_req_header("content-type", "application/json")
619 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
621 assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
625 describe "posting polls" do
626 setup do: oauth_access(["write:statuses"])
628 test "posting a poll", %{conn: conn} do
629 time = NaiveDateTime.utc_now()
633 |> put_req_header("content-type", "application/json")
634 |> post("/api/v1/statuses", %{
635 "status" => "Who is the #bestgrill?",
637 "options" => ["Rei", "Asuka", "Misato"],
642 response = json_response_and_validate_schema(conn, 200)
644 assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
645 title in ["Rei", "Asuka", "Misato"]
648 assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
649 assert response["poll"]["expired"] == false
651 question = Object.get_by_id(response["poll"]["id"])
653 # closed contains utc timezone
654 assert question.data["closed"] =~ "Z"
657 test "option limit is enforced", %{conn: conn} do
658 limit = Config.get([:instance, :poll_limits, :max_options])
662 |> put_req_header("content-type", "application/json")
663 |> post("/api/v1/statuses", %{
665 "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
668 %{"error" => error} = json_response_and_validate_schema(conn, 422)
669 assert error == "Poll can't contain more than #{limit} options"
672 test "option character limit is enforced", %{conn: conn} do
673 limit = Config.get([:instance, :poll_limits, :max_option_chars])
677 |> put_req_header("content-type", "application/json")
678 |> post("/api/v1/statuses", %{
681 "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
686 %{"error" => error} = json_response_and_validate_schema(conn, 422)
687 assert error == "Poll options cannot be longer than #{limit} characters each"
690 test "minimal date limit is enforced", %{conn: conn} do
691 limit = Config.get([:instance, :poll_limits, :min_expiration])
695 |> put_req_header("content-type", "application/json")
696 |> post("/api/v1/statuses", %{
697 "status" => "imagine arbitrary limits",
699 "options" => ["this post was made by pleroma gang"],
700 "expires_in" => limit - 1
704 %{"error" => error} = json_response_and_validate_schema(conn, 422)
705 assert error == "Expiration date is too soon"
708 test "maximum date limit is enforced", %{conn: conn} do
709 limit = Config.get([:instance, :poll_limits, :max_expiration])
713 |> put_req_header("content-type", "application/json")
714 |> post("/api/v1/statuses", %{
715 "status" => "imagine arbitrary limits",
717 "options" => ["this post was made by pleroma gang"],
718 "expires_in" => limit + 1
722 %{"error" => error} = json_response_and_validate_schema(conn, 422)
723 assert error == "Expiration date is too far in the future"
726 test "scheduled poll", %{conn: conn} do
727 clear_config([ScheduledActivity, :enabled], true)
730 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
731 |> NaiveDateTime.to_iso8601()
734 %{"id" => scheduled_id} =
736 |> put_req_header("content-type", "application/json")
737 |> post("/api/v1/statuses", %{
738 "status" => "very cool poll",
740 "options" => ~w(a b c),
743 "scheduled_at" => scheduled_at
745 |> json_response_and_validate_schema(200)
747 assert {:ok, %{id: activity_id}} =
748 perform_job(ScheduledActivityWorker, %{
749 activity_id: scheduled_id
752 refute_enqueued(worker: ScheduledActivityWorker)
756 |> Repo.get(activity_id)
757 |> Object.normalize()
759 assert object.data["content"] == "very cool poll"
760 assert object.data["type"] == "Question"
761 assert length(object.data["oneOf"]) == 3
765 test "get a status" do
766 %{conn: conn} = oauth_access(["read:statuses"])
767 activity = insert(:note_activity)
769 conn = get(conn, "/api/v1/statuses/#{activity.id}")
771 assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
772 assert id == to_string(activity.id)
775 defp local_and_remote_activities do
776 local = insert(:note_activity)
777 remote = insert(:note_activity, local: false)
778 {:ok, local: local, remote: remote}
781 describe "status with restrict unauthenticated activities for local and remote" do
782 setup do: local_and_remote_activities()
784 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
786 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
788 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
789 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
791 assert json_response_and_validate_schema(res_conn, :not_found) == %{
792 "error" => "Record not found"
795 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
797 assert json_response_and_validate_schema(res_conn, :not_found) == %{
798 "error" => "Record not found"
802 test "if user is authenticated", %{local: local, remote: remote} do
803 %{conn: conn} = oauth_access(["read"])
804 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
805 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
807 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
808 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
812 describe "status with restrict unauthenticated activities for local" do
813 setup do: local_and_remote_activities()
815 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
817 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
818 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
820 assert json_response_and_validate_schema(res_conn, :not_found) == %{
821 "error" => "Record not found"
824 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
825 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
828 test "if user is authenticated", %{local: local, remote: remote} do
829 %{conn: conn} = oauth_access(["read"])
830 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
831 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
833 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
834 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
838 describe "status with restrict unauthenticated activities for remote" do
839 setup do: local_and_remote_activities()
841 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
843 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
844 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
845 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
847 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
849 assert json_response_and_validate_schema(res_conn, :not_found) == %{
850 "error" => "Record not found"
854 test "if user is authenticated", %{local: local, remote: remote} do
855 %{conn: conn} = oauth_access(["read"])
856 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
857 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
859 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
860 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
864 test "getting a status that doesn't exist returns 404" do
865 %{conn: conn} = oauth_access(["read:statuses"])
866 activity = insert(:note_activity)
868 conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
870 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
873 test "get a direct status" do
874 %{user: user, conn: conn} = oauth_access(["read:statuses"])
875 other_user = insert(:user)
878 CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
882 |> assign(:user, user)
883 |> get("/api/v1/statuses/#{activity.id}")
885 [participation] = Participation.for_user(user)
887 res = json_response_and_validate_schema(conn, 200)
888 assert res["pleroma"]["direct_conversation_id"] == participation.id
891 test "get statuses by IDs" do
892 %{conn: conn} = oauth_access(["read:statuses"])
893 %{id: id1} = insert(:note_activity)
894 %{id: id2} = insert(:note_activity)
896 query_string = "ids[]=#{id1}&ids[]=#{id2}"
897 conn = get(conn, "/api/v1/statuses/?#{query_string}")
899 assert [%{"id" => ^id1}, %{"id" => ^id2}] =
900 Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
903 describe "getting statuses by ids with restricted unauthenticated for local and remote" do
904 setup do: local_and_remote_activities()
906 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
908 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
910 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
911 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
913 assert json_response_and_validate_schema(res_conn, 200) == []
916 test "if user is authenticated", %{local: local, remote: remote} do
917 %{conn: conn} = oauth_access(["read"])
919 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
921 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
925 describe "getting statuses by ids with restricted unauthenticated for local" do
926 setup do: local_and_remote_activities()
928 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
930 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
931 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
933 remote_id = remote.id
934 assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
937 test "if user is authenticated", %{local: local, remote: remote} do
938 %{conn: conn} = oauth_access(["read"])
940 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
942 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
946 describe "getting statuses by ids with restricted unauthenticated for remote" do
947 setup do: local_and_remote_activities()
949 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
951 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
952 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
955 assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
958 test "if user is authenticated", %{local: local, remote: remote} do
959 %{conn: conn} = oauth_access(["read"])
961 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
963 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
967 describe "deleting a status" do
968 test "when you created it" do
969 %{user: author, conn: conn} = oauth_access(["write:statuses"])
970 activity = insert(:note_activity, user: author)
971 object = Object.normalize(activity, fetch: false)
973 content = object.data["content"]
974 source = object.data["source"]
978 |> assign(:user, author)
979 |> delete("/api/v1/statuses/#{activity.id}")
980 |> json_response_and_validate_schema(200)
982 assert match?(%{"content" => ^content, "text" => ^source}, result)
984 refute Activity.get_by_id(activity.id)
987 test "when it doesn't exist" do
988 %{user: author, conn: conn} = oauth_access(["write:statuses"])
989 activity = insert(:note_activity, user: author)
993 |> assign(:user, author)
994 |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
996 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
999 test "when you didn't create it" do
1000 %{conn: conn} = oauth_access(["write:statuses"])
1001 activity = insert(:note_activity)
1003 conn = delete(conn, "/api/v1/statuses/#{activity.id}")
1005 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
1007 assert Activity.get_by_id(activity.id) == activity
1010 test "when you're an admin or moderator", %{conn: conn} do
1011 activity1 = insert(:note_activity)
1012 activity2 = insert(:note_activity)
1013 admin = insert(:user, is_admin: true)
1014 moderator = insert(:user, is_moderator: true)
1018 |> assign(:user, admin)
1019 |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
1020 |> delete("/api/v1/statuses/#{activity1.id}")
1022 assert %{} = json_response_and_validate_schema(res_conn, 200)
1026 |> assign(:user, moderator)
1027 |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
1028 |> delete("/api/v1/statuses/#{activity2.id}")
1030 assert %{} = json_response_and_validate_schema(res_conn, 200)
1032 refute Activity.get_by_id(activity1.id)
1033 refute Activity.get_by_id(activity2.id)
1037 describe "reblogging" do
1038 setup do: oauth_access(["write:statuses"])
1040 test "reblogs and returns the reblogged status", %{conn: conn} do
1041 activity = insert(:note_activity)
1045 |> put_req_header("content-type", "application/json")
1046 |> post("/api/v1/statuses/#{activity.id}/reblog")
1049 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1051 } = json_response_and_validate_schema(conn, 200)
1053 assert to_string(activity.id) == id
1056 test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
1057 activity = insert(:note_activity)
1061 |> put_req_header("content-type", "application/json")
1062 |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
1064 assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
1067 test "reblogs privately and returns the reblogged status", %{conn: conn} do
1068 activity = insert(:note_activity)
1072 |> put_req_header("content-type", "application/json")
1074 "/api/v1/statuses/#{activity.id}/reblog",
1075 %{"visibility" => "private"}
1079 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1080 "reblogged" => true,
1081 "visibility" => "private"
1082 } = json_response_and_validate_schema(conn, 200)
1084 assert to_string(activity.id) == id
1087 test "reblogged status for another user" do
1088 activity = insert(:note_activity)
1089 user1 = insert(:user)
1090 user2 = insert(:user)
1091 user3 = insert(:user)
1092 {:ok, _} = CommonAPI.favorite(user2, activity.id)
1093 {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
1094 {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
1095 {:ok, _} = CommonAPI.repeat(activity.id, user2)
1099 |> assign(:user, user3)
1100 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1101 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1104 "reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
1105 "reblogged" => false,
1106 "favourited" => false,
1107 "bookmarked" => false
1108 } = json_response_and_validate_schema(conn_res, 200)
1112 |> assign(:user, user2)
1113 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
1114 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1117 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
1118 "reblogged" => true,
1119 "favourited" => true,
1120 "bookmarked" => true
1121 } = json_response_and_validate_schema(conn_res, 200)
1123 assert to_string(activity.id) == id
1126 test "author can reblog own private status", %{conn: conn, user: user} do
1127 {:ok, activity} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
1131 |> put_req_header("content-type", "application/json")
1132 |> post("/api/v1/statuses/#{activity.id}/reblog")
1135 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1136 "reblogged" => true,
1137 "visibility" => "private"
1138 } = json_response_and_validate_schema(conn, 200)
1140 assert to_string(activity.id) == id
1144 describe "unreblogging" do
1145 setup do: oauth_access(["write:statuses"])
1147 test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
1148 activity = insert(:note_activity)
1150 {:ok, _} = CommonAPI.repeat(activity.id, user)
1154 |> put_req_header("content-type", "application/json")
1155 |> post("/api/v1/statuses/#{activity.id}/unreblog")
1157 assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
1158 json_response_and_validate_schema(conn, 200)
1160 assert to_string(activity.id) == id
1163 test "returns 404 error when activity does not exist", %{conn: conn} do
1166 |> put_req_header("content-type", "application/json")
1167 |> post("/api/v1/statuses/foo/unreblog")
1169 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1173 describe "favoriting" do
1174 setup do: oauth_access(["write:favourites"])
1176 test "favs a status and returns it", %{conn: conn} do
1177 activity = insert(:note_activity)
1181 |> put_req_header("content-type", "application/json")
1182 |> post("/api/v1/statuses/#{activity.id}/favourite")
1184 assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
1185 json_response_and_validate_schema(conn, 200)
1187 assert to_string(activity.id) == id
1190 test "favoriting twice will just return 200", %{conn: conn} do
1191 activity = insert(:note_activity)
1194 |> put_req_header("content-type", "application/json")
1195 |> post("/api/v1/statuses/#{activity.id}/favourite")
1198 |> put_req_header("content-type", "application/json")
1199 |> post("/api/v1/statuses/#{activity.id}/favourite")
1200 |> json_response_and_validate_schema(200)
1203 test "returns 404 error for a wrong id", %{conn: conn} do
1206 |> put_req_header("content-type", "application/json")
1207 |> post("/api/v1/statuses/1/favourite")
1209 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1213 describe "unfavoriting" do
1214 setup do: oauth_access(["write:favourites"])
1216 test "unfavorites a status and returns it", %{user: user, conn: conn} do
1217 activity = insert(:note_activity)
1219 {:ok, _} = CommonAPI.favorite(user, activity.id)
1223 |> put_req_header("content-type", "application/json")
1224 |> post("/api/v1/statuses/#{activity.id}/unfavourite")
1226 assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
1227 json_response_and_validate_schema(conn, 200)
1229 assert to_string(activity.id) == id
1232 test "returns 404 error for a wrong id", %{conn: conn} do
1235 |> put_req_header("content-type", "application/json")
1236 |> post("/api/v1/statuses/1/unfavourite")
1238 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1242 describe "pinned statuses" do
1243 setup do: oauth_access(["write:accounts"])
1245 setup %{user: user} do
1246 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
1248 %{activity: activity}
1251 setup do: clear_config([:instance, :max_pinned_statuses], 1)
1253 test "pin status", %{conn: conn, user: user, activity: activity} do
1256 assert %{"id" => ^id, "pinned" => true} =
1258 |> put_req_header("content-type", "application/json")
1259 |> post("/api/v1/statuses/#{activity.id}/pin")
1260 |> json_response_and_validate_schema(200)
1262 assert [%{"id" => ^id, "pinned" => true}] =
1264 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1265 |> json_response_and_validate_schema(200)
1268 test "non authenticated user", %{activity: activity} do
1270 |> put_req_header("content-type", "application/json")
1271 |> post("/api/v1/statuses/#{activity.id}/pin")
1272 |> json_response(403) == %{"error" => "Invalid credentials."}
1275 test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
1276 {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
1280 |> put_req_header("content-type", "application/json")
1281 |> post("/api/v1/statuses/#{dm.id}/pin")
1283 assert json_response_and_validate_schema(conn, 422) == %{
1284 "error" => "Non-public status cannot be pinned"
1288 test "pin by another user", %{activity: activity} do
1289 %{conn: conn} = oauth_access(["write:accounts"])
1292 |> put_req_header("content-type", "application/json")
1293 |> post("/api/v1/statuses/#{activity.id}/pin")
1294 |> json_response(422) == %{"error" => "Someone else's status cannot be pinned"}
1297 test "unpin status", %{conn: conn, user: user, activity: activity} do
1298 {:ok, _} = CommonAPI.pin(activity.id, user)
1299 user = refresh_record(user)
1301 id_str = to_string(activity.id)
1303 assert %{"id" => ^id_str, "pinned" => false} =
1305 |> assign(:user, user)
1306 |> post("/api/v1/statuses/#{activity.id}/unpin")
1307 |> json_response_and_validate_schema(200)
1311 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1312 |> json_response_and_validate_schema(200)
1315 test "/unpin: returns 404 error when activity doesn't exist", %{conn: conn} do
1317 |> put_req_header("content-type", "application/json")
1318 |> post("/api/v1/statuses/1/unpin")
1319 |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
1322 test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
1323 {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
1325 id_str_one = to_string(activity_one.id)
1327 assert %{"id" => ^id_str_one, "pinned" => true} =
1329 |> put_req_header("content-type", "application/json")
1330 |> post("/api/v1/statuses/#{id_str_one}/pin")
1331 |> json_response_and_validate_schema(200)
1333 user = refresh_record(user)
1335 assert %{"error" => "You have already pinned the maximum number of statuses"} =
1337 |> assign(:user, user)
1338 |> post("/api/v1/statuses/#{activity_two.id}/pin")
1339 |> json_response_and_validate_schema(400)
1342 test "on pin removes deletion job, on unpin reschedule deletion" do
1343 %{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
1344 expires_in = 2 * 60 * 60
1346 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
1348 assert %{"id" => id} =
1350 |> put_req_header("content-type", "application/json")
1351 |> post("/api/v1/statuses", %{
1352 "status" => "oolong",
1353 "expires_in" => expires_in
1355 |> json_response_and_validate_schema(200)
1358 worker: Pleroma.Workers.PurgeExpiredActivity,
1359 args: %{activity_id: id},
1360 scheduled_at: expires_at
1363 assert %{"id" => ^id, "pinned" => true} =
1365 |> put_req_header("content-type", "application/json")
1366 |> post("/api/v1/statuses/#{id}/pin")
1367 |> json_response_and_validate_schema(200)
1370 worker: Pleroma.Workers.PurgeExpiredActivity,
1371 args: %{activity_id: id},
1372 scheduled_at: expires_at
1375 assert %{"id" => ^id, "pinned" => false} =
1377 |> put_req_header("content-type", "application/json")
1378 |> post("/api/v1/statuses/#{id}/unpin")
1379 |> json_response_and_validate_schema(200)
1382 worker: Pleroma.Workers.PurgeExpiredActivity,
1383 args: %{activity_id: id},
1384 scheduled_at: expires_at
1390 bookmarks_uri = "/api/v1/bookmarks"
1392 %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
1393 author = insert(:user)
1395 {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
1396 {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
1400 |> put_req_header("content-type", "application/json")
1401 |> post("/api/v1/statuses/#{activity1.id}/bookmark")
1403 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
1407 |> put_req_header("content-type", "application/json")
1408 |> post("/api/v1/statuses/#{activity2.id}/bookmark")
1410 assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
1412 bookmarks = get(conn, bookmarks_uri)
1415 json_response_and_validate_schema(response2, 200),
1416 json_response_and_validate_schema(response1, 200)
1418 json_response_and_validate_schema(bookmarks, 200)
1422 |> put_req_header("content-type", "application/json")
1423 |> post("/api/v1/statuses/#{activity1.id}/unbookmark")
1425 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
1427 bookmarks = get(conn, bookmarks_uri)
1429 assert [json_response_and_validate_schema(response2, 200)] ==
1430 json_response_and_validate_schema(bookmarks, 200)
1433 describe "conversation muting" do
1434 setup do: oauth_access(["write:mutes"])
1437 post_user = insert(:user)
1438 {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
1439 %{activity: activity}
1442 test "mute conversation", %{conn: conn, activity: activity} do
1443 id_str = to_string(activity.id)
1445 assert %{"id" => ^id_str, "muted" => true} =
1447 |> put_req_header("content-type", "application/json")
1448 |> post("/api/v1/statuses/#{activity.id}/mute")
1449 |> json_response_and_validate_schema(200)
1452 test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
1453 {:ok, _} = CommonAPI.add_mute(user, activity)
1457 |> put_req_header("content-type", "application/json")
1458 |> post("/api/v1/statuses/#{activity.id}/mute")
1460 assert json_response_and_validate_schema(conn, 400) == %{
1461 "error" => "conversation is already muted"
1465 test "unmute conversation", %{conn: conn, user: user, activity: activity} do
1466 {:ok, _} = CommonAPI.add_mute(user, activity)
1468 id_str = to_string(activity.id)
1470 assert %{"id" => ^id_str, "muted" => false} =
1472 # |> assign(:user, user)
1473 |> post("/api/v1/statuses/#{activity.id}/unmute")
1474 |> json_response_and_validate_schema(200)
1478 test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
1479 user1 = insert(:user)
1480 user2 = insert(:user)
1481 user3 = insert(:user)
1483 {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
1485 # Reply to status from another user
1488 |> assign(:user, user2)
1489 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
1490 |> put_req_header("content-type", "application/json")
1491 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
1493 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
1495 activity = Activity.get_by_id_with_object(id)
1497 assert Object.normalize(activity, fetch: false).data["inReplyTo"] ==
1498 Object.normalize(replied_to, fetch: false).data["id"]
1500 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
1502 # Reblog from the third user
1505 |> assign(:user, user3)
1506 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
1507 |> put_req_header("content-type", "application/json")
1508 |> post("/api/v1/statuses/#{activity.id}/reblog")
1510 assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
1511 json_response_and_validate_schema(conn2, 200)
1513 assert to_string(activity.id) == id
1515 # Getting third user status
1518 |> assign(:user, user3)
1519 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1520 |> get("/api/v1/timelines/home")
1522 [reblogged_activity] = json_response_and_validate_schema(conn3, 200)
1524 assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
1526 replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
1527 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
1530 describe "GET /api/v1/statuses/:id/favourited_by" do
1531 setup do: oauth_access(["read:accounts"])
1533 setup %{user: user} do
1534 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1536 %{activity: activity}
1539 test "returns users who have favorited the status", %{conn: conn, activity: activity} do
1540 other_user = insert(:user)
1541 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1545 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1546 |> json_response_and_validate_schema(:ok)
1548 [%{"id" => id}] = response
1550 assert id == other_user.id
1553 test "returns empty array when status has not been favorited yet", %{
1559 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1560 |> json_response_and_validate_schema(:ok)
1562 assert Enum.empty?(response)
1565 test "does not return users who have favorited the status but are blocked", %{
1566 conn: %{assigns: %{user: user}} = conn,
1569 other_user = insert(:user)
1570 {:ok, _user_relationship} = User.block(user, other_user)
1572 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1576 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1577 |> json_response_and_validate_schema(:ok)
1579 assert Enum.empty?(response)
1582 test "does not fail on an unauthenticated request", %{activity: activity} do
1583 other_user = insert(:user)
1584 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1588 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1589 |> json_response_and_validate_schema(:ok)
1591 [%{"id" => id}] = response
1592 assert id == other_user.id
1595 test "requires authentication for private posts", %{user: user} do
1596 other_user = insert(:user)
1599 CommonAPI.post(user, %{
1600 status: "@#{other_user.nickname} wanna get some #cofe together?",
1601 visibility: "direct"
1604 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1606 favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
1609 |> get(favourited_by_url)
1610 |> json_response_and_validate_schema(404)
1614 |> assign(:user, other_user)
1615 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1618 |> assign(:token, nil)
1619 |> get(favourited_by_url)
1620 |> json_response_and_validate_schema(404)
1624 |> get(favourited_by_url)
1625 |> json_response_and_validate_schema(200)
1627 [%{"id" => id}] = response
1628 assert id == other_user.id
1631 test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
1632 clear_config([:instance, :show_reactions], false)
1634 other_user = insert(:user)
1635 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1639 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1640 |> json_response_and_validate_schema(:ok)
1642 assert Enum.empty?(response)
1646 describe "GET /api/v1/statuses/:id/reblogged_by" do
1647 setup do: oauth_access(["read:accounts"])
1649 setup %{user: user} do
1650 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1652 %{activity: activity}
1655 test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
1656 other_user = insert(:user)
1657 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1661 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1662 |> json_response_and_validate_schema(:ok)
1664 [%{"id" => id}] = response
1666 assert id == other_user.id
1669 test "returns empty array when status has not been reblogged yet", %{
1675 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1676 |> json_response_and_validate_schema(:ok)
1678 assert Enum.empty?(response)
1681 test "does not return users who have reblogged the status but are blocked", %{
1682 conn: %{assigns: %{user: user}} = conn,
1685 other_user = insert(:user)
1686 {:ok, _user_relationship} = User.block(user, other_user)
1688 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1692 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1693 |> json_response_and_validate_schema(:ok)
1695 assert Enum.empty?(response)
1698 test "does not return users who have reblogged the status privately", %{
1701 other_user = insert(:user)
1702 {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
1704 {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
1708 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1709 |> json_response_and_validate_schema(:ok)
1711 assert Enum.empty?(response)
1714 test "does not fail on an unauthenticated request", %{activity: activity} do
1715 other_user = insert(:user)
1716 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1720 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1721 |> json_response_and_validate_schema(:ok)
1723 [%{"id" => id}] = response
1724 assert id == other_user.id
1727 test "requires authentication for private posts", %{user: user} do
1728 other_user = insert(:user)
1731 CommonAPI.post(user, %{
1732 status: "@#{other_user.nickname} wanna get some #cofe together?",
1733 visibility: "direct"
1737 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1738 |> json_response_and_validate_schema(404)
1742 |> assign(:user, other_user)
1743 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1744 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1745 |> json_response_and_validate_schema(200)
1747 assert [] == response
1752 user = insert(:user)
1754 {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
1755 {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
1756 {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
1757 {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
1758 {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
1762 |> get("/api/v1/statuses/#{id3}/context")
1763 |> json_response_and_validate_schema(:ok)
1766 "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
1767 "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
1771 test "context when restrict_unauthenticated is on" do
1772 user = insert(:user)
1773 remote_user = insert(:user, local: false)
1775 {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
1776 {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
1779 CommonAPI.post(remote_user, %{status: "3", in_reply_to_status_id: id2, local: false})
1783 |> get("/api/v1/statuses/#{id2}/context")
1784 |> json_response_and_validate_schema(:ok)
1787 "ancestors" => [%{"id" => ^id1}],
1788 "descendants" => [%{"id" => ^id3}]
1791 clear_config([:restrict_unauthenticated, :activities, :local], true)
1795 |> get("/api/v1/statuses/#{id2}/context")
1796 |> json_response_and_validate_schema(:ok)
1804 test "favorites paginate correctly" do
1805 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1806 other_user = insert(:user)
1807 {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
1808 {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
1809 {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
1811 {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
1812 {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
1813 {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
1817 |> get("/api/v1/favourites?limit=1")
1819 assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
1820 assert post_id == second_post.id
1822 # Using the header for pagination works correctly
1823 [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
1824 [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next)
1826 assert max_id == third_favorite.id
1830 |> get("/api/v1/favourites?max_id=#{max_id}")
1832 assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
1833 json_response_and_validate_schema(result, 200)
1835 assert first_post_id == first_post.id
1836 assert third_post_id == third_post.id
1839 test "returns the favorites of a user" do
1840 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1841 other_user = insert(:user)
1843 {:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
1844 {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
1846 {:ok, last_like} = CommonAPI.favorite(user, activity.id)
1848 first_conn = get(conn, "/api/v1/favourites")
1850 assert [status] = json_response_and_validate_schema(first_conn, 200)
1851 assert status["id"] == to_string(activity.id)
1853 assert [{"link", _link_header}] =
1854 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
1856 # Honours query params
1857 {:ok, second_activity} =
1858 CommonAPI.post(other_user, %{
1859 status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
1862 {:ok, _} = CommonAPI.favorite(user, second_activity.id)
1864 second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
1866 assert [second_status] = json_response_and_validate_schema(second_conn, 200)
1867 assert second_status["id"] == to_string(second_activity.id)
1869 third_conn = get(conn, "/api/v1/favourites?limit=0")
1871 assert [] = json_response_and_validate_schema(third_conn, 200)
1874 test "expires_at is nil for another user" do
1875 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1876 expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
1877 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
1879 assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
1881 |> get("/api/v1/statuses/#{activity.id}")
1882 |> json_response_and_validate_schema(:ok)
1884 {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
1885 assert DateTime.diff(expires_at, a_expires_at) == 0
1887 %{conn: conn} = oauth_access(["read:statuses"])
1889 assert %{"pleroma" => %{"expires_at" => nil}} =
1891 |> get("/api/v1/statuses/#{activity.id}")
1892 |> json_response_and_validate_schema(:ok)
1895 describe "local-only statuses" do
1896 test "posting a local only status" do
1897 %{user: _user, conn: conn} = oauth_access(["write:statuses"])
1901 |> put_req_header("content-type", "application/json")
1902 |> post("/api/v1/statuses", %{
1904 "visibility" => "local"
1907 local = Utils.as_local_public()
1909 assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
1910 json_response_and_validate_schema(conn_one, 200)
1912 assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
1915 test "other users can read local-only posts" do
1916 user = insert(:user)
1917 %{user: _reader, conn: conn} = oauth_access(["read:statuses"])
1919 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1923 |> get("/api/v1/statuses/#{activity.id}")
1924 |> json_response_and_validate_schema(:ok)
1926 assert received["id"] == activity.id
1929 test "anonymous users cannot see local-only posts" do
1930 user = insert(:user)
1932 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1936 |> get("/api/v1/statuses/#{activity.id}")
1937 |> json_response_and_validate_schema(:not_found)
1941 describe "muted reactions" do
1943 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1945 other_user = insert(:user)
1946 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1948 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
1949 User.mute(user, other_user)
1953 |> get("/api/v1/statuses/?ids[]=#{activity.id}")
1954 |> json_response_and_validate_schema(200)
1959 "emoji_reactions" => []
1966 |> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
1967 |> json_response_and_validate_schema(200)
1972 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
1979 # %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
1980 %{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
1982 other_user = insert(:user)
1983 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1985 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
1986 User.mute(user, other_user)
1990 |> get("/api/v1/statuses/#{activity.id}")
1991 |> json_response_and_validate_schema(200)
1995 "emoji_reactions" => []
2001 |> get("/api/v1/statuses/#{activity.id}?with_muted=true")
2002 |> json_response_and_validate_schema(200)
2006 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
2012 describe "posting quotes" do
2013 setup do: oauth_access(["write:statuses"])
2015 test "posting a quote", %{conn: conn} do
2016 user = insert(:user)
2017 {:ok, quoted_status} = CommonAPI.post(user, %{status: "tell me, for whom do you fight?"})
2021 |> put_req_header("content-type", "application/json")
2022 |> post("/api/v1/statuses", %{
2023 "status" => "Hmph, how very glib",
2024 "quote_id" => quoted_status.id
2027 response = json_response_and_validate_schema(conn, 200)
2029 assert response["quote_id"] == quoted_status.id
2030 assert response["quote"]["id"] == quoted_status.id
2031 assert response["quote"]["content"] == quoted_status.object.data["content"]
2032 assert response["pleroma"]["context"] == quoted_status.data["context"]
2035 test "posting a quote, quoting a status that isn't public", %{conn: conn} do
2036 user = insert(:user)
2038 Enum.each(["private", "local", "direct"], fn visibility ->
2039 {:ok, quoted_status} =
2040 CommonAPI.post(user, %{
2041 status: "tell me, for whom do you fight?",
2042 visibility: visibility
2045 assert %{"error" => "You can only quote public or unlisted statuses"} =
2047 |> put_req_header("content-type", "application/json")
2048 |> post("/api/v1/statuses", %{
2049 "status" => "Hmph, how very glib",
2050 "quote_id" => quoted_status.id
2052 |> json_response_and_validate_schema(422)
2056 test "posting a quote, after quote, the status gets deleted", %{conn: conn} do
2057 user = insert(:user)
2059 {:ok, quoted_status} =
2060 CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
2064 |> put_req_header("content-type", "application/json")
2065 |> post("/api/v1/statuses", %{
2066 "status" => "I fight for eorzea!",
2067 "quote_id" => quoted_status.id
2069 |> json_response_and_validate_schema(200)
2071 {:ok, _} = CommonAPI.delete(quoted_status.id, user)
2075 |> get("/api/v1/statuses/#{resp["id"]}")
2076 |> json_response_and_validate_schema(200)
2078 assert is_nil(resp["quote"])
2081 test "posting a quote of a deleted status", %{conn: conn} do
2082 user = insert(:user)
2084 {:ok, quoted_status} =
2085 CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
2087 {:ok, _} = CommonAPI.delete(quoted_status.id, user)
2089 assert %{"error" => _} =
2091 |> put_req_header("content-type", "application/json")
2092 |> post("/api/v1/statuses", %{
2093 "status" => "I fight for eorzea!",
2094 "quote_id" => quoted_status.id
2096 |> json_response_and_validate_schema(422)
2099 test "posting a quote of a status that doesn't exist", %{conn: conn} do
2100 assert %{"error" => "You can't quote a status that doesn't exist"} =
2102 |> put_req_header("content-type", "application/json")
2103 |> post("/api/v1/statuses", %{
2104 "status" => "I fight for eorzea!",
2105 "quote_id" => "oops"
2107 |> json_response_and_validate_schema(422)
2111 describe "get status history" do
2113 %{conn: build_conn()}
2116 test "unedited post", %{conn: conn} do
2117 activity = insert(:note_activity)
2119 conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
2121 assert [_] = json_response_and_validate_schema(conn, 200)
2124 test "edited post", %{conn: conn} do
2129 "formerRepresentations" => %{
2130 "type" => "OrderedCollection",
2134 "content" => "mew mew 2",
2135 "summary" => "title 2"
2139 "content" => "mew mew 1",
2140 "summary" => "title 1"
2148 activity = insert(:note_activity, note: note)
2150 conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
2152 assert [%{"spoiler_text" => "title 1"}, %{"spoiler_text" => "title 2"}, _] =
2153 json_response_and_validate_schema(conn, 200)
2157 describe "translating statuses" do
2159 clear_config([:translator, :enabled], true)
2160 clear_config([:translator, :module], Pleroma.Akkoma.Translators.DeepL)
2161 clear_config([:deepl, :api_key], "deepl_api_key")
2162 oauth_access(["read:statuses"])
2165 test "listing languages", %{conn: conn} do
2166 Tesla.Mock.mock_global(fn
2167 %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=source"} ->
2172 %{language: "en", name: "English"}
2176 %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=target"} ->
2181 %{language: "ja", name: "Japanese"}
2188 |> put_req_header("content-type", "application/json")
2189 |> get("/api/v1/akkoma/translation/languages")
2191 response = json_response_and_validate_schema(conn, 200)
2194 "source" => [%{"code" => "en", "name" => "English"}],
2195 "target" => [%{"code" => "ja", "name" => "Japanese"}]
2199 test "should return text and detected language", %{conn: conn} do
2200 clear_config([:deepl, :tier], :free)
2202 Tesla.Mock.mock_global(fn
2203 %{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
2210 "text" => "Tell me, for whom do you fight?",
2211 "detected_source_language" => "ja"
2218 user = insert(:user)
2219 {:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?"})
2223 |> put_req_header("content-type", "application/json")
2224 |> get("/api/v1/statuses/#{to_translate.id}/translations/en")
2226 response = json_response_and_validate_schema(conn, 200)
2228 assert response["text"] == "Tell me, for whom do you fight?"
2229 assert response["detected_language"] == "ja"
2232 test "should not allow translating of statuses you cannot see", %{conn: conn} do
2233 clear_config([:deepl, :tier], :free)
2235 Tesla.Mock.mock_global(fn
2236 %{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
2243 "text" => "Tell me, for whom do you fight?",
2244 "detected_source_language" => "ja"
2251 user = insert(:user)
2252 {:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?", visibility: "private"})
2256 |> put_req_header("content-type", "application/json")
2257 |> get("/api/v1/statuses/#{to_translate.id}/translations/en")
2259 json_response_and_validate_schema(conn, 404)
2263 describe "get status source" do
2265 %{conn: build_conn()}
2268 test "it returns the source", %{conn: conn} do
2269 user = insert(:user)
2271 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2273 conn = get(conn, "/api/v1/statuses/#{activity.id}/source")
2277 assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
2278 json_response_and_validate_schema(conn, 200)
2282 describe "update status" do
2284 oauth_access(["write:statuses"])
2287 test "it updates the status" do
2288 %{conn: conn, user: user} = oauth_access(["write:statuses", "read:statuses"])
2290 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2293 |> get("/api/v1/statuses/#{activity.id}")
2294 |> json_response_and_validate_schema(200)
2298 |> put_req_header("content-type", "application/json")
2299 |> put("/api/v1/statuses/#{activity.id}", %{
2300 "status" => "edited",
2301 "spoiler_text" => "lol"
2303 |> json_response_and_validate_schema(200)
2305 assert response["content"] == "edited"
2306 assert response["spoiler_text"] == "lol"
2310 |> get("/api/v1/statuses/#{activity.id}")
2311 |> json_response_and_validate_schema(200)
2313 assert response["content"] == "edited"
2314 assert response["spoiler_text"] == "lol"
2317 test "it updates the attachments", %{conn: conn, user: user} do
2318 attachment = insert(:attachment, user: user)
2319 attachment_id = to_string(attachment.id)
2321 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2325 |> put_req_header("content-type", "application/json")
2326 |> put("/api/v1/statuses/#{activity.id}", %{
2327 "status" => "mew mew #abc",
2328 "spoiler_text" => "#def",
2329 "media_ids" => [attachment_id]
2331 |> json_response_and_validate_schema(200)
2333 assert [%{"id" => ^attachment_id}] = response["media_attachments"]
2336 test "it does not update visibility", %{conn: conn, user: user} do
2338 CommonAPI.post(user, %{
2339 status: "mew mew #abc",
2340 spoiler_text: "#def",
2341 visibility: "private"
2346 |> put_req_header("content-type", "application/json")
2347 |> put("/api/v1/statuses/#{activity.id}", %{
2348 "status" => "edited",
2349 "spoiler_text" => "lol"
2351 |> json_response_and_validate_schema(200)
2353 assert response["visibility"] == "private"
2356 test "it refuses to update when original post is not by the user", %{conn: conn} do
2357 another_user = insert(:user)
2360 CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
2363 |> put_req_header("content-type", "application/json")
2364 |> put("/api/v1/statuses/#{activity.id}", %{
2365 "status" => "edited",
2366 "spoiler_text" => "lol"
2368 |> json_response_and_validate_schema(:forbidden)
2371 test "it returns 404 if the user cannot see the post", %{conn: conn} do
2372 another_user = insert(:user)
2375 CommonAPI.post(another_user, %{
2376 status: "mew mew #abc",
2377 spoiler_text: "#def",
2378 visibility: "private"
2382 |> put_req_header("content-type", "application/json")
2383 |> put("/api/v1/statuses/#{activity.id}", %{
2384 "status" => "edited",
2385 "spoiler_text" => "lol"
2387 |> json_response_and_validate_schema(:not_found)