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",
73 assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
74 json_response_and_validate_schema(conn_one, 200)
76 assert Activity.get_by_id(id)
80 |> put_req_header("content-type", "application/json")
81 |> put_req_header("idempotency-key", idempotency_key)
82 |> post("/api/v1/statuses", %{
84 "spoiler_text" => "2hu",
88 # Idempotency plug response means detection fail
89 assert %{"id" => second_id} = json_response(conn_two, 200)
90 assert id == second_id
94 |> put_req_header("content-type", "application/json")
95 |> post("/api/v1/statuses", %{
97 "spoiler_text" => "2hu",
98 "sensitive" => "False"
101 assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
102 refute id == third_id
104 # An activity that will expire:
106 expires_in = 2 * 60 * 60
108 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
112 |> put_req_header("content-type", "application/json")
113 |> post("/api/v1/statuses", %{
114 "status" => "oolong",
115 "expires_in" => expires_in
118 assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
120 assert Activity.get_by_id(fourth_id)
123 worker: Pleroma.Workers.PurgeExpiredActivity,
124 args: %{activity_id: fourth_id},
125 scheduled_at: expires_at
129 test "automatically setting a post expiry if status_ttl_days is set" do
130 user = insert(:user, status_ttl_days: 1)
131 %{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
135 |> put_req_header("content-type", "application/json")
136 |> post("/api/v1/statuses", %{
137 "status" => "aa chikichiki banban"
140 assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
142 activity = Activity.get_by_id_with_object(id)
143 {:ok, expires_at, _} = DateTime.from_iso8601(activity.data["expires_at"])
152 worker: Pleroma.Workers.PurgeExpiredActivity,
153 args: %{activity_id: id},
154 scheduled_at: DateTime.add(DateTime.utc_now(), 1 * 60 * 60 * 24)
158 test "it fails to create a status if `expires_in` is less or equal than an hour", %{
164 assert %{"error" => "Expiry date is too soon"} =
166 |> put_req_header("content-type", "application/json")
167 |> post("/api/v1/statuses", %{
168 "status" => "oolong",
169 "expires_in" => expires_in
171 |> json_response_and_validate_schema(422)
176 assert %{"error" => "Expiry date is too soon"} =
178 |> put_req_header("content-type", "application/json")
179 |> post("/api/v1/statuses", %{
180 "status" => "oolong",
181 "expires_in" => expires_in
183 |> json_response_and_validate_schema(422)
186 test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
187 clear_config([:mrf_keyword, :reject], ["GNO"])
188 clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
190 assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
192 |> put_req_header("content-type", "application/json")
193 |> post("/api/v1/statuses", %{"status" => "GNO/Linux"})
194 |> json_response_and_validate_schema(422)
197 test "posting an undefined status with an attachment", %{user: user, conn: conn} do
199 content_type: "image/jpeg",
200 path: Path.absname("test/fixtures/image.jpg"),
201 filename: "an_image.jpg"
204 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
208 |> put_req_header("content-type", "application/json")
209 |> post("/api/v1/statuses", %{
210 "media_ids" => [to_string(upload.id)]
213 assert json_response_and_validate_schema(conn, 200)
216 test "replying to a status", %{user: user, conn: conn} do
217 {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
221 |> put_req_header("content-type", "application/json")
222 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
224 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
226 activity = Activity.get_by_id(id)
228 assert activity.data["context"] == replied_to.data["context"]
229 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
232 test "replying to a direct message with visibility other than direct", %{
236 {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
238 Enum.each(["public", "private", "unlisted"], fn visibility ->
241 |> put_req_header("content-type", "application/json")
242 |> post("/api/v1/statuses", %{
243 "status" => "@#{user.nickname} hey",
244 "in_reply_to_id" => replied_to.id,
245 "visibility" => visibility
248 assert json_response_and_validate_schema(conn, 422) == %{
249 "error" => "The message visibility must be direct"
254 test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
257 |> put_req_header("content-type", "application/json")
258 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
260 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
261 assert Activity.get_by_id(id)
264 test "posting a sensitive status", %{conn: conn} do
267 |> put_req_header("content-type", "application/json")
268 |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
270 assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
271 json_response_and_validate_schema(conn, 200)
273 assert Activity.get_by_id(id)
276 test "posting a fake status", %{conn: conn} do
279 |> put_req_header("content-type", "application/json")
280 |> post("/api/v1/statuses", %{
282 "\"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"
285 real_status = json_response_and_validate_schema(real_conn, 200)
288 assert Object.get_by_ap_id(real_status["uri"])
292 |> Map.put("id", nil)
293 |> Map.put("url", nil)
294 |> Map.put("uri", nil)
295 |> Map.put("created_at", nil)
296 |> Kernel.put_in(["pleroma", "context"], nil)
297 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
301 |> assign(:user, refresh_record(conn.assigns.user))
302 |> put_req_header("content-type", "application/json")
303 |> post("/api/v1/statuses", %{
305 "\"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",
309 fake_status = json_response_and_validate_schema(fake_conn, 200)
312 refute Object.get_by_ap_id(fake_status["uri"])
316 |> Map.put("id", nil)
317 |> Map.put("url", nil)
318 |> Map.put("uri", nil)
319 |> Map.put("created_at", nil)
320 |> Kernel.put_in(["pleroma", "context"], nil)
321 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
323 assert real_status == fake_status
326 test "fake statuses' preview card is not cached", %{conn: conn} do
327 clear_config([:rich_media, :enabled], true)
329 Tesla.Mock.mock_global(fn
332 url: "https://example.com/twitter-card"
334 %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
337 apply(HttpRequestMock, :request, [env])
342 |> put_req_header("content-type", "application/json")
343 |> post("/api/v1/statuses", %{
344 "status" => "https://example.com/ogp",
350 |> put_req_header("content-type", "application/json")
351 |> post("/api/v1/statuses", %{
352 "status" => "https://example.com/twitter-card",
356 assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200)
358 assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} =
359 json_response_and_validate_schema(conn2, 200)
362 test "posting a status with OGP link preview", %{conn: conn} do
363 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
364 clear_config([:rich_media, :enabled], true)
368 |> put_req_header("content-type", "application/json")
369 |> post("/api/v1/statuses", %{
370 "status" => "https://example.com/ogp"
373 assert %{"id" => id, "card" => %{"title" => "The Rock"}} =
374 json_response_and_validate_schema(conn, 200)
376 assert Activity.get_by_id(id)
379 test "posting a direct status", %{conn: conn} do
380 user2 = insert(:user)
381 content = "direct cofe @#{user2.nickname}"
385 |> put_req_header("content-type", "application/json")
386 |> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"})
388 assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
389 assert response["visibility"] == "direct"
390 assert response["pleroma"]["direct_conversation_id"]
391 assert activity = Activity.get_by_id(id)
392 assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
393 assert activity.data["to"] == [user2.ap_id]
394 assert activity.data["cc"] == []
397 test "discloses application metadata when enabled" do
398 user = insert(:user, disclose_client: true)
399 %{user: _user, token: token, conn: conn} = oauth_access(["write:statuses"], user: user)
401 %Pleroma.Web.OAuth.Token{
402 app: %Pleroma.Web.OAuth.App{
403 client_name: app_name,
410 |> put_req_header("content-type", "application/json")
411 |> post("/api/v1/statuses", %{
412 "status" => "cofe is my copilot"
416 "content" => "cofe is my copilot"
417 } = json_response_and_validate_schema(result, 200)
419 activity = result.assigns.activity.id
423 |> get("/api/v1/statuses/#{activity}")
426 "content" => "cofe is my copilot",
429 "website" => ^app_website
431 } = json_response_and_validate_schema(result, 200)
434 test "hides application metadata when disabled" do
435 user = insert(:user, disclose_client: false)
436 %{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
440 |> put_req_header("content-type", "application/json")
441 |> post("/api/v1/statuses", %{
442 "status" => "club mate is my wingman"
445 assert %{"content" => "club mate is my wingman"} =
446 json_response_and_validate_schema(result, 200)
448 activity = result.assigns.activity.id
452 |> get("/api/v1/statuses/#{activity}")
455 "content" => "club mate is my wingman",
457 } = json_response_and_validate_schema(result, 200)
461 describe "posting scheduled statuses" do
462 setup do: oauth_access(["write:statuses"])
464 test "creates a scheduled activity", %{conn: conn} do
466 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
467 |> NaiveDateTime.to_iso8601()
472 |> put_req_header("content-type", "application/json")
473 |> post("/api/v1/statuses", %{
474 "status" => "scheduled",
475 "scheduled_at" => scheduled_at
478 assert %{"scheduled_at" => expected_scheduled_at} =
479 json_response_and_validate_schema(conn, 200)
481 assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
482 assert [] == Repo.all(Activity)
485 test "with expiration" do
486 %{conn: conn} = oauth_access(["write:statuses", "read:statuses"])
489 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
490 |> NaiveDateTime.to_iso8601()
493 assert %{"id" => status_id, "params" => %{"expires_in" => 300}} =
495 |> put_req_header("content-type", "application/json")
496 |> post("/api/v1/statuses", %{
497 "status" => "scheduled",
498 "scheduled_at" => scheduled_at,
501 |> json_response_and_validate_schema(200)
503 assert %{"id" => ^status_id, "params" => %{"expires_in" => 300}} =
505 |> put_req_header("content-type", "application/json")
506 |> get("/api/v1/scheduled_statuses/#{status_id}")
507 |> json_response_and_validate_schema(200)
510 test "ignores nil values", %{conn: conn} do
513 |> put_req_header("content-type", "application/json")
514 |> post("/api/v1/statuses", %{
515 "status" => "not scheduled",
516 "scheduled_at" => nil
519 assert result = json_response_and_validate_schema(conn, 200)
520 assert Activity.get_by_id(result["id"])
523 test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
525 NaiveDateTime.utc_now()
526 |> NaiveDateTime.add(:timer.minutes(120), :millisecond)
527 |> NaiveDateTime.to_iso8601()
531 content_type: "image/jpeg",
532 path: Path.absname("test/fixtures/image.jpg"),
533 filename: "an_image.jpg"
536 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
540 |> put_req_header("content-type", "application/json")
541 |> post("/api/v1/statuses", %{
542 "media_ids" => [to_string(upload.id)],
543 "status" => "scheduled",
544 "scheduled_at" => scheduled_at
547 assert %{"media_attachments" => [media_attachment]} =
548 json_response_and_validate_schema(conn, 200)
550 assert %{"type" => "image"} = media_attachment
553 test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
556 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
557 |> NaiveDateTime.to_iso8601()
562 |> put_req_header("content-type", "application/json")
563 |> post("/api/v1/statuses", %{
564 "status" => "not scheduled",
565 "scheduled_at" => scheduled_at
568 assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
569 assert [] == Repo.all(ScheduledActivity)
572 test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
574 NaiveDateTime.utc_now()
575 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
576 |> NaiveDateTime.to_iso8601()
580 attrs = %{params: %{}, scheduled_at: today}
581 {:ok, _} = ScheduledActivity.create(user, attrs)
582 {:ok, _} = ScheduledActivity.create(user, attrs)
586 |> put_req_header("content-type", "application/json")
587 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
589 assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
592 test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
594 NaiveDateTime.utc_now()
595 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
596 |> NaiveDateTime.to_iso8601()
600 NaiveDateTime.utc_now()
601 |> NaiveDateTime.add(:timer.hours(36), :millisecond)
602 |> NaiveDateTime.to_iso8601()
605 attrs = %{params: %{}, scheduled_at: today}
606 {:ok, _} = ScheduledActivity.create(user, attrs)
607 {:ok, _} = ScheduledActivity.create(user, attrs)
608 {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
612 |> put_req_header("content-type", "application/json")
613 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
615 assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
619 describe "posting polls" do
620 setup do: oauth_access(["write:statuses"])
622 test "posting a poll", %{conn: conn} do
623 time = NaiveDateTime.utc_now()
627 |> put_req_header("content-type", "application/json")
628 |> post("/api/v1/statuses", %{
629 "status" => "Who is the #bestgrill?",
631 "options" => ["Rei", "Asuka", "Misato"],
636 response = json_response_and_validate_schema(conn, 200)
638 assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
639 title in ["Rei", "Asuka", "Misato"]
642 assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
643 assert response["poll"]["expired"] == false
645 question = Object.get_by_id(response["poll"]["id"])
647 # closed contains utc timezone
648 assert question.data["closed"] =~ "Z"
651 test "option limit is enforced", %{conn: conn} do
652 limit = Config.get([:instance, :poll_limits, :max_options])
656 |> put_req_header("content-type", "application/json")
657 |> post("/api/v1/statuses", %{
659 "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
662 %{"error" => error} = json_response_and_validate_schema(conn, 422)
663 assert error == "Poll can't contain more than #{limit} options"
666 test "option character limit is enforced", %{conn: conn} do
667 limit = Config.get([:instance, :poll_limits, :max_option_chars])
671 |> put_req_header("content-type", "application/json")
672 |> post("/api/v1/statuses", %{
675 "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
680 %{"error" => error} = json_response_and_validate_schema(conn, 422)
681 assert error == "Poll options cannot be longer than #{limit} characters each"
684 test "minimal date limit is enforced", %{conn: conn} do
685 limit = Config.get([:instance, :poll_limits, :min_expiration])
689 |> put_req_header("content-type", "application/json")
690 |> post("/api/v1/statuses", %{
691 "status" => "imagine arbitrary limits",
693 "options" => ["this post was made by pleroma gang"],
694 "expires_in" => limit - 1
698 %{"error" => error} = json_response_and_validate_schema(conn, 422)
699 assert error == "Expiration date is too soon"
702 test "maximum date limit is enforced", %{conn: conn} do
703 limit = Config.get([:instance, :poll_limits, :max_expiration])
707 |> put_req_header("content-type", "application/json")
708 |> post("/api/v1/statuses", %{
709 "status" => "imagine arbitrary limits",
711 "options" => ["this post was made by pleroma gang"],
712 "expires_in" => limit + 1
716 %{"error" => error} = json_response_and_validate_schema(conn, 422)
717 assert error == "Expiration date is too far in the future"
720 test "scheduled poll", %{conn: conn} do
721 clear_config([ScheduledActivity, :enabled], true)
724 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
725 |> NaiveDateTime.to_iso8601()
728 %{"id" => scheduled_id} =
730 |> put_req_header("content-type", "application/json")
731 |> post("/api/v1/statuses", %{
732 "status" => "very cool poll",
734 "options" => ~w(a b c),
737 "scheduled_at" => scheduled_at
739 |> json_response_and_validate_schema(200)
741 assert {:ok, %{id: activity_id}} =
742 perform_job(ScheduledActivityWorker, %{
743 activity_id: scheduled_id
746 refute_enqueued(worker: ScheduledActivityWorker)
750 |> Repo.get(activity_id)
751 |> Object.normalize()
753 assert object.data["content"] == "very cool poll"
754 assert object.data["type"] == "Question"
755 assert length(object.data["oneOf"]) == 3
759 test "get a status" do
760 %{conn: conn} = oauth_access(["read:statuses"])
761 activity = insert(:note_activity)
763 conn = get(conn, "/api/v1/statuses/#{activity.id}")
765 assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
766 assert id == to_string(activity.id)
769 defp local_and_remote_activities do
770 local = insert(:note_activity)
771 remote = insert(:note_activity, local: false)
772 {:ok, local: local, remote: remote}
775 describe "status with restrict unauthenticated activities for local and remote" do
776 setup do: local_and_remote_activities()
778 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
780 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
782 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
783 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
785 assert json_response_and_validate_schema(res_conn, :not_found) == %{
786 "error" => "Record not found"
789 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
791 assert json_response_and_validate_schema(res_conn, :not_found) == %{
792 "error" => "Record not found"
796 test "if user is authenticated", %{local: local, remote: remote} do
797 %{conn: conn} = oauth_access(["read"])
798 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
799 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
801 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
802 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
806 describe "status with restrict unauthenticated activities for local" do
807 setup do: local_and_remote_activities()
809 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
811 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
812 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
814 assert json_response_and_validate_schema(res_conn, :not_found) == %{
815 "error" => "Record not found"
818 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
819 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
822 test "if user is authenticated", %{local: local, remote: remote} do
823 %{conn: conn} = oauth_access(["read"])
824 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
825 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
827 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
828 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
832 describe "status with restrict unauthenticated activities for remote" do
833 setup do: local_and_remote_activities()
835 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
837 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
838 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
839 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
841 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
843 assert json_response_and_validate_schema(res_conn, :not_found) == %{
844 "error" => "Record not found"
848 test "if user is authenticated", %{local: local, remote: remote} do
849 %{conn: conn} = oauth_access(["read"])
850 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
851 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
853 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
854 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
858 test "getting a status that doesn't exist returns 404" do
859 %{conn: conn} = oauth_access(["read:statuses"])
860 activity = insert(:note_activity)
862 conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
864 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
867 test "get a direct status" do
868 %{user: user, conn: conn} = oauth_access(["read:statuses"])
869 other_user = insert(:user)
872 CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
876 |> assign(:user, user)
877 |> get("/api/v1/statuses/#{activity.id}")
879 [participation] = Participation.for_user(user)
881 res = json_response_and_validate_schema(conn, 200)
882 assert res["pleroma"]["direct_conversation_id"] == participation.id
885 test "get statuses by IDs" do
886 %{conn: conn} = oauth_access(["read:statuses"])
887 %{id: id1} = insert(:note_activity)
888 %{id: id2} = insert(:note_activity)
890 query_string = "ids[]=#{id1}&ids[]=#{id2}"
891 conn = get(conn, "/api/v1/statuses/?#{query_string}")
893 assert [%{"id" => ^id1}, %{"id" => ^id2}] =
894 Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
897 describe "getting statuses by ids with restricted unauthenticated for local and remote" do
898 setup do: local_and_remote_activities()
900 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
902 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
904 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
905 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
907 assert json_response_and_validate_schema(res_conn, 200) == []
910 test "if user is authenticated", %{local: local, remote: remote} do
911 %{conn: conn} = oauth_access(["read"])
913 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
915 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
919 describe "getting statuses by ids with restricted unauthenticated for local" do
920 setup do: local_and_remote_activities()
922 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
924 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
925 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
927 remote_id = remote.id
928 assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
931 test "if user is authenticated", %{local: local, remote: remote} do
932 %{conn: conn} = oauth_access(["read"])
934 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
936 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
940 describe "getting statuses by ids with restricted unauthenticated for remote" do
941 setup do: local_and_remote_activities()
943 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
945 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
946 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
949 assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
952 test "if user is authenticated", %{local: local, remote: remote} do
953 %{conn: conn} = oauth_access(["read"])
955 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
957 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
961 describe "deleting a status" do
962 test "when you created it" do
963 %{user: author, conn: conn} = oauth_access(["write:statuses"])
964 activity = insert(:note_activity, user: author)
965 object = Object.normalize(activity, fetch: false)
967 content = object.data["content"]
968 source = object.data["source"]
972 |> assign(:user, author)
973 |> delete("/api/v1/statuses/#{activity.id}")
974 |> json_response_and_validate_schema(200)
976 assert match?(%{"content" => ^content, "text" => ^source}, result)
978 refute Activity.get_by_id(activity.id)
981 test "when it doesn't exist" do
982 %{user: author, conn: conn} = oauth_access(["write:statuses"])
983 activity = insert(:note_activity, user: author)
987 |> assign(:user, author)
988 |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
990 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
993 test "when you didn't create it" do
994 %{conn: conn} = oauth_access(["write:statuses"])
995 activity = insert(:note_activity)
997 conn = delete(conn, "/api/v1/statuses/#{activity.id}")
999 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
1001 assert Activity.get_by_id(activity.id) == activity
1004 test "when you're an admin or moderator", %{conn: conn} do
1005 activity1 = insert(:note_activity)
1006 activity2 = insert(:note_activity)
1007 admin = insert(:user, is_admin: true)
1008 moderator = insert(:user, is_moderator: true)
1012 |> assign(:user, admin)
1013 |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
1014 |> delete("/api/v1/statuses/#{activity1.id}")
1016 assert %{} = json_response_and_validate_schema(res_conn, 200)
1020 |> assign(:user, moderator)
1021 |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
1022 |> delete("/api/v1/statuses/#{activity2.id}")
1024 assert %{} = json_response_and_validate_schema(res_conn, 200)
1026 refute Activity.get_by_id(activity1.id)
1027 refute Activity.get_by_id(activity2.id)
1031 describe "reblogging" do
1032 setup do: oauth_access(["write:statuses"])
1034 test "reblogs and returns the reblogged status", %{conn: conn} do
1035 activity = insert(:note_activity)
1039 |> put_req_header("content-type", "application/json")
1040 |> post("/api/v1/statuses/#{activity.id}/reblog")
1043 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1045 } = json_response_and_validate_schema(conn, 200)
1047 assert to_string(activity.id) == id
1050 test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
1051 activity = insert(:note_activity)
1055 |> put_req_header("content-type", "application/json")
1056 |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
1058 assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
1061 test "reblogs privately and returns the reblogged status", %{conn: conn} do
1062 activity = insert(:note_activity)
1066 |> put_req_header("content-type", "application/json")
1068 "/api/v1/statuses/#{activity.id}/reblog",
1069 %{"visibility" => "private"}
1073 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1074 "reblogged" => true,
1075 "visibility" => "private"
1076 } = json_response_and_validate_schema(conn, 200)
1078 assert to_string(activity.id) == id
1081 test "reblogged status for another user" do
1082 activity = insert(:note_activity)
1083 user1 = insert(:user)
1084 user2 = insert(:user)
1085 user3 = insert(:user)
1086 {:ok, _} = CommonAPI.favorite(user2, activity.id)
1087 {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
1088 {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
1089 {:ok, _} = CommonAPI.repeat(activity.id, user2)
1093 |> assign(:user, user3)
1094 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1095 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1098 "reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
1099 "reblogged" => false,
1100 "favourited" => false,
1101 "bookmarked" => false
1102 } = json_response_and_validate_schema(conn_res, 200)
1106 |> assign(:user, user2)
1107 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
1108 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1111 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
1112 "reblogged" => true,
1113 "favourited" => true,
1114 "bookmarked" => true
1115 } = json_response_and_validate_schema(conn_res, 200)
1117 assert to_string(activity.id) == id
1120 test "author can reblog own private status", %{conn: conn, user: user} do
1121 {:ok, activity} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
1125 |> put_req_header("content-type", "application/json")
1126 |> post("/api/v1/statuses/#{activity.id}/reblog")
1129 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1130 "reblogged" => true,
1131 "visibility" => "private"
1132 } = json_response_and_validate_schema(conn, 200)
1134 assert to_string(activity.id) == id
1138 describe "unreblogging" do
1139 setup do: oauth_access(["write:statuses"])
1141 test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
1142 activity = insert(:note_activity)
1144 {:ok, _} = CommonAPI.repeat(activity.id, user)
1148 |> put_req_header("content-type", "application/json")
1149 |> post("/api/v1/statuses/#{activity.id}/unreblog")
1151 assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
1152 json_response_and_validate_schema(conn, 200)
1154 assert to_string(activity.id) == id
1157 test "returns 404 error when activity does not exist", %{conn: conn} do
1160 |> put_req_header("content-type", "application/json")
1161 |> post("/api/v1/statuses/foo/unreblog")
1163 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1167 describe "favoriting" do
1168 setup do: oauth_access(["write:favourites"])
1170 test "favs a status and returns it", %{conn: conn} do
1171 activity = insert(:note_activity)
1175 |> put_req_header("content-type", "application/json")
1176 |> post("/api/v1/statuses/#{activity.id}/favourite")
1178 assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
1179 json_response_and_validate_schema(conn, 200)
1181 assert to_string(activity.id) == id
1184 test "favoriting twice will just return 200", %{conn: conn} do
1185 activity = insert(:note_activity)
1188 |> put_req_header("content-type", "application/json")
1189 |> post("/api/v1/statuses/#{activity.id}/favourite")
1192 |> put_req_header("content-type", "application/json")
1193 |> post("/api/v1/statuses/#{activity.id}/favourite")
1194 |> json_response_and_validate_schema(200)
1197 test "returns 404 error for a wrong id", %{conn: conn} do
1200 |> put_req_header("content-type", "application/json")
1201 |> post("/api/v1/statuses/1/favourite")
1203 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1207 describe "unfavoriting" do
1208 setup do: oauth_access(["write:favourites"])
1210 test "unfavorites a status and returns it", %{user: user, conn: conn} do
1211 activity = insert(:note_activity)
1213 {:ok, _} = CommonAPI.favorite(user, activity.id)
1217 |> put_req_header("content-type", "application/json")
1218 |> post("/api/v1/statuses/#{activity.id}/unfavourite")
1220 assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
1221 json_response_and_validate_schema(conn, 200)
1223 assert to_string(activity.id) == id
1226 test "returns 404 error for a wrong id", %{conn: conn} do
1229 |> put_req_header("content-type", "application/json")
1230 |> post("/api/v1/statuses/1/unfavourite")
1232 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1236 describe "pinned statuses" do
1237 setup do: oauth_access(["write:accounts"])
1239 setup %{user: user} do
1240 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
1242 %{activity: activity}
1245 setup do: clear_config([:instance, :max_pinned_statuses], 1)
1247 test "pin status", %{conn: conn, user: user, activity: activity} do
1250 assert %{"id" => ^id, "pinned" => true} =
1252 |> put_req_header("content-type", "application/json")
1253 |> post("/api/v1/statuses/#{activity.id}/pin")
1254 |> json_response_and_validate_schema(200)
1256 assert [%{"id" => ^id, "pinned" => true}] =
1258 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1259 |> json_response_and_validate_schema(200)
1262 test "non authenticated user", %{activity: activity} do
1264 |> put_req_header("content-type", "application/json")
1265 |> post("/api/v1/statuses/#{activity.id}/pin")
1266 |> json_response(403) == %{"error" => "Invalid credentials."}
1269 test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
1270 {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
1274 |> put_req_header("content-type", "application/json")
1275 |> post("/api/v1/statuses/#{dm.id}/pin")
1277 assert json_response_and_validate_schema(conn, 422) == %{
1278 "error" => "Non-public status cannot be pinned"
1282 test "pin by another user", %{activity: activity} do
1283 %{conn: conn} = oauth_access(["write:accounts"])
1286 |> put_req_header("content-type", "application/json")
1287 |> post("/api/v1/statuses/#{activity.id}/pin")
1288 |> json_response(422) == %{"error" => "Someone else's status cannot be pinned"}
1291 test "unpin status", %{conn: conn, user: user, activity: activity} do
1292 {:ok, _} = CommonAPI.pin(activity.id, user)
1293 user = refresh_record(user)
1295 id_str = to_string(activity.id)
1297 assert %{"id" => ^id_str, "pinned" => false} =
1299 |> assign(:user, user)
1300 |> post("/api/v1/statuses/#{activity.id}/unpin")
1301 |> json_response_and_validate_schema(200)
1305 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1306 |> json_response_and_validate_schema(200)
1309 test "/unpin: returns 404 error when activity doesn't exist", %{conn: conn} do
1311 |> put_req_header("content-type", "application/json")
1312 |> post("/api/v1/statuses/1/unpin")
1313 |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
1316 test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
1317 {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
1319 id_str_one = to_string(activity_one.id)
1321 assert %{"id" => ^id_str_one, "pinned" => true} =
1323 |> put_req_header("content-type", "application/json")
1324 |> post("/api/v1/statuses/#{id_str_one}/pin")
1325 |> json_response_and_validate_schema(200)
1327 user = refresh_record(user)
1329 assert %{"error" => "You have already pinned the maximum number of statuses"} =
1331 |> assign(:user, user)
1332 |> post("/api/v1/statuses/#{activity_two.id}/pin")
1333 |> json_response_and_validate_schema(400)
1336 test "on pin removes deletion job, on unpin reschedule deletion" do
1337 %{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
1338 expires_in = 2 * 60 * 60
1340 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
1342 assert %{"id" => id} =
1344 |> put_req_header("content-type", "application/json")
1345 |> post("/api/v1/statuses", %{
1346 "status" => "oolong",
1347 "expires_in" => expires_in
1349 |> json_response_and_validate_schema(200)
1352 worker: Pleroma.Workers.PurgeExpiredActivity,
1353 args: %{activity_id: id},
1354 scheduled_at: expires_at
1357 assert %{"id" => ^id, "pinned" => true} =
1359 |> put_req_header("content-type", "application/json")
1360 |> post("/api/v1/statuses/#{id}/pin")
1361 |> json_response_and_validate_schema(200)
1364 worker: Pleroma.Workers.PurgeExpiredActivity,
1365 args: %{activity_id: id},
1366 scheduled_at: expires_at
1369 assert %{"id" => ^id, "pinned" => false} =
1371 |> put_req_header("content-type", "application/json")
1372 |> post("/api/v1/statuses/#{id}/unpin")
1373 |> json_response_and_validate_schema(200)
1376 worker: Pleroma.Workers.PurgeExpiredActivity,
1377 args: %{activity_id: id},
1378 scheduled_at: expires_at
1384 bookmarks_uri = "/api/v1/bookmarks"
1386 %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
1387 author = insert(:user)
1389 {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
1390 {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
1394 |> put_req_header("content-type", "application/json")
1395 |> post("/api/v1/statuses/#{activity1.id}/bookmark")
1397 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
1401 |> put_req_header("content-type", "application/json")
1402 |> post("/api/v1/statuses/#{activity2.id}/bookmark")
1404 assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
1406 bookmarks = get(conn, bookmarks_uri)
1409 json_response_and_validate_schema(response2, 200),
1410 json_response_and_validate_schema(response1, 200)
1412 json_response_and_validate_schema(bookmarks, 200)
1416 |> put_req_header("content-type", "application/json")
1417 |> post("/api/v1/statuses/#{activity1.id}/unbookmark")
1419 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
1421 bookmarks = get(conn, bookmarks_uri)
1423 assert [json_response_and_validate_schema(response2, 200)] ==
1424 json_response_and_validate_schema(bookmarks, 200)
1427 describe "conversation muting" do
1428 setup do: oauth_access(["write:mutes"])
1431 post_user = insert(:user)
1432 {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
1433 %{activity: activity}
1436 test "mute conversation", %{conn: conn, activity: activity} do
1437 id_str = to_string(activity.id)
1439 assert %{"id" => ^id_str, "muted" => true} =
1441 |> put_req_header("content-type", "application/json")
1442 |> post("/api/v1/statuses/#{activity.id}/mute")
1443 |> json_response_and_validate_schema(200)
1446 test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
1447 {:ok, _} = CommonAPI.add_mute(user, activity)
1451 |> put_req_header("content-type", "application/json")
1452 |> post("/api/v1/statuses/#{activity.id}/mute")
1454 assert json_response_and_validate_schema(conn, 400) == %{
1455 "error" => "conversation is already muted"
1459 test "unmute conversation", %{conn: conn, user: user, activity: activity} do
1460 {:ok, _} = CommonAPI.add_mute(user, activity)
1462 id_str = to_string(activity.id)
1464 assert %{"id" => ^id_str, "muted" => false} =
1466 # |> assign(:user, user)
1467 |> post("/api/v1/statuses/#{activity.id}/unmute")
1468 |> json_response_and_validate_schema(200)
1472 test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
1473 user1 = insert(:user)
1474 user2 = insert(:user)
1475 user3 = insert(:user)
1477 {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
1479 # Reply to status from another user
1482 |> assign(:user, user2)
1483 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
1484 |> put_req_header("content-type", "application/json")
1485 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
1487 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
1489 activity = Activity.get_by_id_with_object(id)
1491 assert Object.normalize(activity, fetch: false).data["inReplyTo"] ==
1492 Object.normalize(replied_to, fetch: false).data["id"]
1494 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
1496 # Reblog from the third user
1499 |> assign(:user, user3)
1500 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
1501 |> put_req_header("content-type", "application/json")
1502 |> post("/api/v1/statuses/#{activity.id}/reblog")
1504 assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
1505 json_response_and_validate_schema(conn2, 200)
1507 assert to_string(activity.id) == id
1509 # Getting third user status
1512 |> assign(:user, user3)
1513 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1514 |> get("/api/v1/timelines/home")
1516 [reblogged_activity] = json_response_and_validate_schema(conn3, 200)
1518 assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
1520 replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
1521 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
1524 describe "GET /api/v1/statuses/:id/favourited_by" do
1525 setup do: oauth_access(["read:accounts"])
1527 setup %{user: user} do
1528 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1530 %{activity: activity}
1533 test "returns users who have favorited the status", %{conn: conn, activity: activity} do
1534 other_user = insert(:user)
1535 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1539 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1540 |> json_response_and_validate_schema(:ok)
1542 [%{"id" => id}] = response
1544 assert id == other_user.id
1547 test "returns empty array when status has not been favorited yet", %{
1553 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1554 |> json_response_and_validate_schema(:ok)
1556 assert Enum.empty?(response)
1559 test "does not return users who have favorited the status but are blocked", %{
1560 conn: %{assigns: %{user: user}} = conn,
1563 other_user = insert(:user)
1564 {:ok, _user_relationship} = User.block(user, other_user)
1566 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1570 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1571 |> json_response_and_validate_schema(:ok)
1573 assert Enum.empty?(response)
1576 test "does not fail on an unauthenticated request", %{activity: activity} do
1577 other_user = insert(:user)
1578 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1582 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1583 |> json_response_and_validate_schema(:ok)
1585 [%{"id" => id}] = response
1586 assert id == other_user.id
1589 test "requires authentication for private posts", %{user: user} do
1590 other_user = insert(:user)
1593 CommonAPI.post(user, %{
1594 status: "@#{other_user.nickname} wanna get some #cofe together?",
1595 visibility: "direct"
1598 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1600 favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
1603 |> get(favourited_by_url)
1604 |> json_response_and_validate_schema(404)
1608 |> assign(:user, other_user)
1609 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1612 |> assign(:token, nil)
1613 |> get(favourited_by_url)
1614 |> json_response_and_validate_schema(404)
1618 |> get(favourited_by_url)
1619 |> json_response_and_validate_schema(200)
1621 [%{"id" => id}] = response
1622 assert id == other_user.id
1625 test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
1626 clear_config([:instance, :show_reactions], false)
1628 other_user = insert(:user)
1629 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1633 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1634 |> json_response_and_validate_schema(:ok)
1636 assert Enum.empty?(response)
1640 describe "GET /api/v1/statuses/:id/reblogged_by" do
1641 setup do: oauth_access(["read:accounts"])
1643 setup %{user: user} do
1644 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1646 %{activity: activity}
1649 test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
1650 other_user = insert(:user)
1651 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1655 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1656 |> json_response_and_validate_schema(:ok)
1658 [%{"id" => id}] = response
1660 assert id == other_user.id
1663 test "returns empty array when status has not been reblogged yet", %{
1669 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1670 |> json_response_and_validate_schema(:ok)
1672 assert Enum.empty?(response)
1675 test "does not return users who have reblogged the status but are blocked", %{
1676 conn: %{assigns: %{user: user}} = conn,
1679 other_user = insert(:user)
1680 {:ok, _user_relationship} = User.block(user, other_user)
1682 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1686 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1687 |> json_response_and_validate_schema(:ok)
1689 assert Enum.empty?(response)
1692 test "does not return users who have reblogged the status privately", %{
1695 other_user = insert(:user)
1696 {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
1698 {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
1702 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1703 |> json_response_and_validate_schema(:ok)
1705 assert Enum.empty?(response)
1708 test "does not fail on an unauthenticated request", %{activity: activity} do
1709 other_user = insert(:user)
1710 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1714 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1715 |> json_response_and_validate_schema(:ok)
1717 [%{"id" => id}] = response
1718 assert id == other_user.id
1721 test "requires authentication for private posts", %{user: user} do
1722 other_user = insert(:user)
1725 CommonAPI.post(user, %{
1726 status: "@#{other_user.nickname} wanna get some #cofe together?",
1727 visibility: "direct"
1731 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1732 |> json_response_and_validate_schema(404)
1736 |> assign(:user, other_user)
1737 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1738 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1739 |> json_response_and_validate_schema(200)
1741 assert [] == response
1746 user = insert(:user)
1748 {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
1749 {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
1750 {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
1751 {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
1752 {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
1756 |> get("/api/v1/statuses/#{id3}/context")
1757 |> json_response_and_validate_schema(:ok)
1760 "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
1761 "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
1765 test "context when restrict_unauthenticated is on" do
1766 user = insert(:user)
1767 remote_user = insert(:user, local: false)
1769 {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
1770 {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
1773 CommonAPI.post(remote_user, %{status: "3", in_reply_to_status_id: id2, local: false})
1777 |> get("/api/v1/statuses/#{id2}/context")
1778 |> json_response_and_validate_schema(:ok)
1781 "ancestors" => [%{"id" => ^id1}],
1782 "descendants" => [%{"id" => ^id3}]
1785 clear_config([:restrict_unauthenticated, :activities, :local], true)
1789 |> get("/api/v1/statuses/#{id2}/context")
1790 |> json_response_and_validate_schema(:ok)
1798 test "favorites paginate correctly" do
1799 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1800 other_user = insert(:user)
1801 {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
1802 {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
1803 {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
1805 {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
1806 {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
1807 {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
1811 |> get("/api/v1/favourites?limit=1")
1813 assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
1814 assert post_id == second_post.id
1816 # Using the header for pagination works correctly
1817 [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
1818 [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next)
1820 assert max_id == third_favorite.id
1824 |> get("/api/v1/favourites?max_id=#{max_id}")
1826 assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
1827 json_response_and_validate_schema(result, 200)
1829 assert first_post_id == first_post.id
1830 assert third_post_id == third_post.id
1833 test "returns the favorites of a user" do
1834 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1835 other_user = insert(:user)
1837 {:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
1838 {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
1840 {:ok, last_like} = CommonAPI.favorite(user, activity.id)
1842 first_conn = get(conn, "/api/v1/favourites")
1844 assert [status] = json_response_and_validate_schema(first_conn, 200)
1845 assert status["id"] == to_string(activity.id)
1847 assert [{"link", _link_header}] =
1848 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
1850 # Honours query params
1851 {:ok, second_activity} =
1852 CommonAPI.post(other_user, %{
1853 status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
1856 {:ok, _} = CommonAPI.favorite(user, second_activity.id)
1858 second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
1860 assert [second_status] = json_response_and_validate_schema(second_conn, 200)
1861 assert second_status["id"] == to_string(second_activity.id)
1863 third_conn = get(conn, "/api/v1/favourites?limit=0")
1865 assert [] = json_response_and_validate_schema(third_conn, 200)
1868 test "expires_at is nil for another user" do
1869 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1870 expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
1871 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
1873 assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
1875 |> get("/api/v1/statuses/#{activity.id}")
1876 |> json_response_and_validate_schema(:ok)
1878 {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
1879 assert DateTime.diff(expires_at, a_expires_at) == 0
1881 %{conn: conn} = oauth_access(["read:statuses"])
1883 assert %{"pleroma" => %{"expires_at" => nil}} =
1885 |> get("/api/v1/statuses/#{activity.id}")
1886 |> json_response_and_validate_schema(:ok)
1889 describe "local-only statuses" do
1890 test "posting a local only status" do
1891 %{user: _user, conn: conn} = oauth_access(["write:statuses"])
1895 |> put_req_header("content-type", "application/json")
1896 |> post("/api/v1/statuses", %{
1898 "visibility" => "local"
1901 local = Utils.as_local_public()
1903 assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
1904 json_response_and_validate_schema(conn_one, 200)
1906 assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
1909 test "other users can read local-only posts" do
1910 user = insert(:user)
1911 %{user: _reader, conn: conn} = oauth_access(["read:statuses"])
1913 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1917 |> get("/api/v1/statuses/#{activity.id}")
1918 |> json_response_and_validate_schema(:ok)
1920 assert received["id"] == activity.id
1923 test "anonymous users cannot see local-only posts" do
1924 user = insert(:user)
1926 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1930 |> get("/api/v1/statuses/#{activity.id}")
1931 |> json_response_and_validate_schema(:not_found)
1935 describe "muted reactions" do
1937 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1939 other_user = insert(:user)
1940 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1942 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
1943 User.mute(user, other_user)
1947 |> get("/api/v1/statuses/?ids[]=#{activity.id}")
1948 |> json_response_and_validate_schema(200)
1953 "emoji_reactions" => []
1960 |> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
1961 |> json_response_and_validate_schema(200)
1966 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
1973 # %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
1974 %{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
1976 other_user = insert(:user)
1977 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1979 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
1980 User.mute(user, other_user)
1984 |> get("/api/v1/statuses/#{activity.id}")
1985 |> json_response_and_validate_schema(200)
1989 "emoji_reactions" => []
1995 |> get("/api/v1/statuses/#{activity.id}?with_muted=true")
1996 |> json_response_and_validate_schema(200)
2000 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
2006 describe "posting quotes" do
2007 setup do: oauth_access(["write:statuses"])
2009 test "posting a quote", %{conn: conn} do
2010 user = insert(:user)
2011 {:ok, quoted_status} = CommonAPI.post(user, %{status: "tell me, for whom do you fight?"})
2015 |> put_req_header("content-type", "application/json")
2016 |> post("/api/v1/statuses", %{
2017 "status" => "Hmph, how very glib",
2018 "quote_id" => quoted_status.id
2021 response = json_response_and_validate_schema(conn, 200)
2023 assert response["quote_id"] == quoted_status.id
2024 assert response["quote"]["id"] == quoted_status.id
2025 assert response["quote"]["content"] == quoted_status.object.data["content"]
2026 assert response["pleroma"]["context"] == quoted_status.data["context"]
2029 test "posting a quote, quoting a status that isn't public", %{conn: conn} do
2030 user = insert(:user)
2032 Enum.each(["private", "local", "direct"], fn visibility ->
2033 {:ok, quoted_status} =
2034 CommonAPI.post(user, %{
2035 status: "tell me, for whom do you fight?",
2036 visibility: visibility
2039 assert %{"error" => "You can only quote public or unlisted statuses"} =
2041 |> put_req_header("content-type", "application/json")
2042 |> post("/api/v1/statuses", %{
2043 "status" => "Hmph, how very glib",
2044 "quote_id" => quoted_status.id
2046 |> json_response_and_validate_schema(422)
2050 test "posting a quote, after quote, the status gets deleted", %{conn: conn} do
2051 user = insert(:user)
2053 {:ok, quoted_status} =
2054 CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
2058 |> put_req_header("content-type", "application/json")
2059 |> post("/api/v1/statuses", %{
2060 "status" => "I fight for eorzea!",
2061 "quote_id" => quoted_status.id
2063 |> json_response_and_validate_schema(200)
2065 {:ok, _} = CommonAPI.delete(quoted_status.id, user)
2069 |> get("/api/v1/statuses/#{resp["id"]}")
2070 |> json_response_and_validate_schema(200)
2072 assert is_nil(resp["quote"])
2075 test "posting a quote of a deleted status", %{conn: conn} do
2076 user = insert(:user)
2078 {:ok, quoted_status} =
2079 CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
2081 {:ok, _} = CommonAPI.delete(quoted_status.id, user)
2083 assert %{"error" => _} =
2085 |> put_req_header("content-type", "application/json")
2086 |> post("/api/v1/statuses", %{
2087 "status" => "I fight for eorzea!",
2088 "quote_id" => quoted_status.id
2090 |> json_response_and_validate_schema(422)
2093 test "posting a quote of a status that doesn't exist", %{conn: conn} do
2094 assert %{"error" => "You can't quote a status that doesn't exist"} =
2096 |> put_req_header("content-type", "application/json")
2097 |> post("/api/v1/statuses", %{
2098 "status" => "I fight for eorzea!",
2099 "quote_id" => "oops"
2101 |> json_response_and_validate_schema(422)
2105 describe "get status history" do
2107 %{conn: build_conn()}
2110 test "unedited post", %{conn: conn} do
2111 activity = insert(:note_activity)
2113 conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
2115 assert [_] = json_response_and_validate_schema(conn, 200)
2118 test "edited post", %{conn: conn} do
2123 "formerRepresentations" => %{
2124 "type" => "OrderedCollection",
2128 "content" => "mew mew 2",
2129 "summary" => "title 2"
2133 "content" => "mew mew 1",
2134 "summary" => "title 1"
2142 activity = insert(:note_activity, note: note)
2144 conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
2146 assert [%{"spoiler_text" => "title 1"}, %{"spoiler_text" => "title 2"}, _] =
2147 json_response_and_validate_schema(conn, 200)
2151 describe "translating statuses" do
2153 clear_config([:translator, :enabled], true)
2154 clear_config([:translator, :module], Pleroma.Akkoma.Translators.DeepL)
2155 clear_config([:deepl, :api_key], "deepl_api_key")
2156 oauth_access(["read:statuses"])
2159 test "listing languages", %{conn: conn} do
2160 Tesla.Mock.mock_global(fn
2161 %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=source"} ->
2166 %{language: "en", name: "English"}
2170 %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=target"} ->
2175 %{language: "ja", name: "Japanese"}
2182 |> put_req_header("content-type", "application/json")
2183 |> get("/api/v1/akkoma/translation/languages")
2185 response = json_response_and_validate_schema(conn, 200)
2188 "source" => [%{"code" => "en", "name" => "English"}],
2189 "target" => [%{"code" => "ja", "name" => "Japanese"}]
2193 test "should return text and detected language", %{conn: conn} do
2194 clear_config([:deepl, :tier], :free)
2196 Tesla.Mock.mock_global(fn
2197 %{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
2204 "text" => "Tell me, for whom do you fight?",
2205 "detected_source_language" => "ja"
2212 user = insert(:user)
2213 {:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?"})
2217 |> put_req_header("content-type", "application/json")
2218 |> get("/api/v1/statuses/#{to_translate.id}/translations/en")
2220 response = json_response_and_validate_schema(conn, 200)
2222 assert response["text"] == "Tell me, for whom do you fight?"
2223 assert response["detected_language"] == "ja"
2226 test "should not allow translating of statuses you cannot see", %{conn: conn} do
2227 clear_config([:deepl, :tier], :free)
2229 Tesla.Mock.mock_global(fn
2230 %{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
2237 "text" => "Tell me, for whom do you fight?",
2238 "detected_source_language" => "ja"
2245 user = insert(:user)
2246 {:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?", visibility: "private"})
2250 |> put_req_header("content-type", "application/json")
2251 |> get("/api/v1/statuses/#{to_translate.id}/translations/en")
2253 json_response_and_validate_schema(conn, 404)
2257 describe "get status source" do
2259 %{conn: build_conn()}
2262 test "it returns the source", %{conn: conn} do
2263 user = insert(:user)
2265 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2267 conn = get(conn, "/api/v1/statuses/#{activity.id}/source")
2271 assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
2272 json_response_and_validate_schema(conn, 200)
2276 describe "update status" do
2278 oauth_access(["write:statuses"])
2281 test "it updates the status" do
2282 %{conn: conn, user: user} = oauth_access(["write:statuses", "read:statuses"])
2284 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2287 |> get("/api/v1/statuses/#{activity.id}")
2288 |> json_response_and_validate_schema(200)
2292 |> put_req_header("content-type", "application/json")
2293 |> put("/api/v1/statuses/#{activity.id}", %{
2294 "status" => "edited",
2295 "spoiler_text" => "lol"
2297 |> json_response_and_validate_schema(200)
2299 assert response["content"] == "edited"
2300 assert response["spoiler_text"] == "lol"
2304 |> get("/api/v1/statuses/#{activity.id}")
2305 |> json_response_and_validate_schema(200)
2307 assert response["content"] == "edited"
2308 assert response["spoiler_text"] == "lol"
2311 test "it updates the attachments", %{conn: conn, user: user} do
2312 attachment = insert(:attachment, user: user)
2313 attachment_id = to_string(attachment.id)
2315 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2319 |> put_req_header("content-type", "application/json")
2320 |> put("/api/v1/statuses/#{activity.id}", %{
2321 "status" => "mew mew #abc",
2322 "spoiler_text" => "#def",
2323 "media_ids" => [attachment_id]
2325 |> json_response_and_validate_schema(200)
2327 assert [%{"id" => ^attachment_id}] = response["media_attachments"]
2330 test "it does not update visibility", %{conn: conn, user: user} do
2332 CommonAPI.post(user, %{
2333 status: "mew mew #abc",
2334 spoiler_text: "#def",
2335 visibility: "private"
2340 |> put_req_header("content-type", "application/json")
2341 |> put("/api/v1/statuses/#{activity.id}", %{
2342 "status" => "edited",
2343 "spoiler_text" => "lol"
2345 |> json_response_and_validate_schema(200)
2347 assert response["visibility"] == "private"
2350 test "it refuses to update when original post is not by the user", %{conn: conn} do
2351 another_user = insert(:user)
2354 CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
2357 |> put_req_header("content-type", "application/json")
2358 |> put("/api/v1/statuses/#{activity.id}", %{
2359 "status" => "edited",
2360 "spoiler_text" => "lol"
2362 |> json_response_and_validate_schema(:forbidden)
2365 test "it returns 404 if the user cannot see the post", %{conn: conn} do
2366 another_user = insert(:user)
2369 CommonAPI.post(another_user, %{
2370 status: "mew mew #abc",
2371 spoiler_text: "#def",
2372 visibility: "private"
2376 |> put_req_header("content-type", "application/json")
2377 |> put("/api/v1/statuses/#{activity.id}", %{
2378 "status" => "edited",
2379 "spoiler_text" => "lol"
2381 |> json_response_and_validate_schema(:not_found)