1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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, async: false
7 use Oban.Testing, repo: Pleroma.Repo
10 alias Pleroma.Conversation.Participation
11 alias Pleroma.ModerationLog
14 alias Pleroma.ScheduledActivity
15 alias Pleroma.Tests.ObanHelpers
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Workers.ScheduledActivityWorker
22 import Pleroma.Factory
24 setup do: clear_config([:instance, :federating])
25 setup do: clear_config([:instance, :allow_relay])
26 setup do: clear_config([:rich_media, :enabled])
27 setup do: clear_config([:mrf, :policies])
28 setup do: clear_config([:mrf_keyword, :reject])
30 describe "posting statuses" do
31 setup do: oauth_access(["write:statuses"])
33 test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
34 clear_config([:instance, :federating], true)
35 Config.get([:instance, :allow_relay], true)
39 |> put_req_header("content-type", "application/json")
40 |> post("api/v1/statuses", %{
41 "content_type" => "text/plain",
42 "source" => "Pleroma FE",
43 "status" => "Hello world",
44 "visibility" => "public"
46 |> json_response_and_validate_schema(200)
48 assert response["reblogs_count"] == 0
49 ObanHelpers.perform_all()
53 |> get("api/v1/statuses/#{response["id"]}", %{})
54 |> json_response_and_validate_schema(200)
56 assert response["reblogs_count"] == 0
59 test "posting a status", %{conn: conn} do
60 idempotency_key = "Pikachu rocks!"
64 |> put_req_header("content-type", "application/json")
65 |> put_req_header("idempotency-key", idempotency_key)
66 |> post("/api/v1/statuses", %{
68 "spoiler_text" => "2hu",
72 assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
73 json_response_and_validate_schema(conn_one, 200)
75 assert Activity.get_by_id(id)
79 |> put_req_header("content-type", "application/json")
80 |> put_req_header("idempotency-key", idempotency_key)
81 |> post("/api/v1/statuses", %{
83 "spoiler_text" => "2hu",
87 # Idempotency plug response means detection fail
88 assert %{"id" => second_id} = json_response(conn_two, 200)
89 assert id == second_id
93 |> put_req_header("content-type", "application/json")
94 |> post("/api/v1/statuses", %{
96 "spoiler_text" => "2hu",
97 "sensitive" => "False"
100 assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
101 refute id == third_id
103 # An activity that will expire:
105 expires_in = 2 * 60 * 60
107 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
111 |> put_req_header("content-type", "application/json")
112 |> post("api/v1/statuses", %{
113 "status" => "oolong",
114 "expires_in" => expires_in
117 assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
119 assert Activity.get_by_id(fourth_id)
122 worker: Pleroma.Workers.PurgeExpiredActivity,
123 args: %{activity_id: fourth_id},
124 scheduled_at: expires_at
128 test "it fails to create a status if `expires_in` is less or equal than an hour", %{
134 assert %{"error" => "Expiry date is too soon"} =
136 |> put_req_header("content-type", "application/json")
137 |> post("api/v1/statuses", %{
138 "status" => "oolong",
139 "expires_in" => expires_in
141 |> json_response_and_validate_schema(422)
146 assert %{"error" => "Expiry date is too soon"} =
148 |> put_req_header("content-type", "application/json")
149 |> post("api/v1/statuses", %{
150 "status" => "oolong",
151 "expires_in" => expires_in
153 |> json_response_and_validate_schema(422)
156 test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
157 clear_config([:mrf_keyword, :reject], ["GNO"])
158 clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
160 assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
162 |> put_req_header("content-type", "application/json")
163 |> post("api/v1/statuses", %{"status" => "GNO/Linux"})
164 |> json_response_and_validate_schema(422)
167 test "posting an undefined status with an attachment", %{user: user, conn: conn} do
169 content_type: "image/jpeg",
170 path: Path.absname("test/fixtures/image.jpg"),
171 filename: "an_image.jpg"
174 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
178 |> put_req_header("content-type", "application/json")
179 |> post("/api/v1/statuses", %{
180 "media_ids" => [to_string(upload.id)]
183 assert json_response_and_validate_schema(conn, 200)
186 test "replying to a status", %{user: user, conn: conn} do
187 {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
191 |> put_req_header("content-type", "application/json")
192 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
194 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
196 activity = Activity.get_by_id(id)
198 assert activity.data["context"] == replied_to.data["context"]
199 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
202 test "replying to a direct message with visibility other than direct", %{
206 {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
208 Enum.each(["public", "private", "unlisted"], fn visibility ->
211 |> put_req_header("content-type", "application/json")
212 |> post("/api/v1/statuses", %{
213 "status" => "@#{user.nickname} hey",
214 "in_reply_to_id" => replied_to.id,
215 "visibility" => visibility
218 assert json_response_and_validate_schema(conn, 422) == %{
219 "error" => "The message visibility must be direct"
224 test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
227 |> put_req_header("content-type", "application/json")
228 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
230 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
231 assert Activity.get_by_id(id)
234 test "posting a sensitive status", %{conn: conn} do
237 |> put_req_header("content-type", "application/json")
238 |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
240 assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
241 json_response_and_validate_schema(conn, 200)
243 assert Activity.get_by_id(id)
246 test "posting a fake status", %{conn: conn} do
249 |> put_req_header("content-type", "application/json")
250 |> post("/api/v1/statuses", %{
252 "\"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"
255 real_status = json_response_and_validate_schema(real_conn, 200)
258 assert Object.get_by_ap_id(real_status["uri"])
262 |> Map.put("id", nil)
263 |> Map.put("url", nil)
264 |> Map.put("uri", nil)
265 |> Map.put("created_at", nil)
266 |> Kernel.put_in(["pleroma", "context"], nil)
267 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
271 |> assign(:user, refresh_record(conn.assigns.user))
272 |> put_req_header("content-type", "application/json")
273 |> post("/api/v1/statuses", %{
275 "\"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",
279 fake_status = json_response_and_validate_schema(fake_conn, 200)
282 refute Object.get_by_ap_id(fake_status["uri"])
286 |> Map.put("id", nil)
287 |> Map.put("url", nil)
288 |> Map.put("uri", nil)
289 |> Map.put("created_at", nil)
290 |> Kernel.put_in(["pleroma", "context"], nil)
291 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
293 assert real_status == fake_status
296 test "fake statuses' preview card is not cached", %{conn: conn} do
297 clear_config([:rich_media, :enabled], true)
302 url: "https://example.com/twitter-card"
304 %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
307 apply(HttpRequestMock, :request, [env])
312 |> put_req_header("content-type", "application/json")
313 |> post("/api/v1/statuses", %{
314 "status" => "https://example.com/ogp",
320 |> put_req_header("content-type", "application/json")
321 |> post("/api/v1/statuses", %{
322 "status" => "https://example.com/twitter-card",
326 assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200)
328 assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} =
329 json_response_and_validate_schema(conn2, 200)
332 test "posting a status with OGP link preview", %{conn: conn} do
333 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
334 clear_config([:rich_media, :enabled], true)
338 |> put_req_header("content-type", "application/json")
339 |> post("/api/v1/statuses", %{
340 "status" => "https://example.com/ogp"
343 assert %{"id" => id, "card" => %{"title" => "The Rock"}} =
344 json_response_and_validate_schema(conn, 200)
346 assert Activity.get_by_id(id)
349 test "posting a direct status", %{conn: conn} do
350 user2 = insert(:user)
351 content = "direct cofe @#{user2.nickname}"
355 |> put_req_header("content-type", "application/json")
356 |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
358 assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
359 assert response["visibility"] == "direct"
360 assert response["pleroma"]["direct_conversation_id"]
361 assert activity = Activity.get_by_id(id)
362 assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
363 assert activity.data["to"] == [user2.ap_id]
364 assert activity.data["cc"] == []
367 test "discloses application metadata when enabled" do
368 user = insert(:user, disclose_client: true)
369 %{user: _user, token: token, conn: conn} = oauth_access(["write:statuses"], user: user)
371 %Pleroma.Web.OAuth.Token{
372 app: %Pleroma.Web.OAuth.App{
373 client_name: app_name,
380 |> put_req_header("content-type", "application/json")
381 |> post("/api/v1/statuses", %{
382 "status" => "cofe is my copilot"
386 "content" => "cofe is my copilot"
387 } = json_response_and_validate_schema(result, 200)
389 activity = result.assigns.activity.id
393 |> get("api/v1/statuses/#{activity}")
396 "content" => "cofe is my copilot",
399 "website" => ^app_website
401 } = json_response_and_validate_schema(result, 200)
404 test "hides application metadata when disabled" do
405 user = insert(:user, disclose_client: false)
406 %{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
410 |> put_req_header("content-type", "application/json")
411 |> post("/api/v1/statuses", %{
412 "status" => "club mate is my wingman"
415 assert %{"content" => "club mate is my wingman"} =
416 json_response_and_validate_schema(result, 200)
418 activity = result.assigns.activity.id
422 |> get("api/v1/statuses/#{activity}")
425 "content" => "club mate is my wingman",
427 } = json_response_and_validate_schema(result, 200)
431 describe "posting scheduled statuses" do
432 setup do: oauth_access(["write:statuses"])
434 test "creates a scheduled activity", %{conn: conn} do
436 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
437 |> NaiveDateTime.to_iso8601()
442 |> put_req_header("content-type", "application/json")
443 |> post("/api/v1/statuses", %{
444 "status" => "scheduled",
445 "scheduled_at" => scheduled_at
448 assert %{"scheduled_at" => expected_scheduled_at} =
449 json_response_and_validate_schema(conn, 200)
451 assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
452 assert [] == Repo.all(Activity)
455 test "with expiration" do
456 %{conn: conn} = oauth_access(["write:statuses", "read:statuses"])
459 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
460 |> NaiveDateTime.to_iso8601()
463 assert %{"id" => status_id, "params" => %{"expires_in" => 300}} =
465 |> put_req_header("content-type", "application/json")
466 |> post("/api/v1/statuses", %{
467 "status" => "scheduled",
468 "scheduled_at" => scheduled_at,
471 |> json_response_and_validate_schema(200)
473 assert %{"id" => ^status_id, "params" => %{"expires_in" => 300}} =
475 |> put_req_header("content-type", "application/json")
476 |> get("/api/v1/scheduled_statuses/#{status_id}")
477 |> json_response_and_validate_schema(200)
480 test "ignores nil values", %{conn: conn} do
483 |> put_req_header("content-type", "application/json")
484 |> post("/api/v1/statuses", %{
485 "status" => "not scheduled",
486 "scheduled_at" => nil
489 assert result = json_response_and_validate_schema(conn, 200)
490 assert Activity.get_by_id(result["id"])
493 test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
495 NaiveDateTime.utc_now()
496 |> NaiveDateTime.add(:timer.minutes(120), :millisecond)
497 |> NaiveDateTime.to_iso8601()
501 content_type: "image/jpeg",
502 path: Path.absname("test/fixtures/image.jpg"),
503 filename: "an_image.jpg"
506 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
510 |> put_req_header("content-type", "application/json")
511 |> post("/api/v1/statuses", %{
512 "media_ids" => [to_string(upload.id)],
513 "status" => "scheduled",
514 "scheduled_at" => scheduled_at
517 assert %{"media_attachments" => [media_attachment]} =
518 json_response_and_validate_schema(conn, 200)
520 assert %{"type" => "image"} = media_attachment
523 test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
526 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
527 |> NaiveDateTime.to_iso8601()
532 |> put_req_header("content-type", "application/json")
533 |> post("/api/v1/statuses", %{
534 "status" => "not scheduled",
535 "scheduled_at" => scheduled_at
538 assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
539 assert [] == Repo.all(ScheduledActivity)
542 test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
544 NaiveDateTime.utc_now()
545 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
546 |> NaiveDateTime.to_iso8601()
550 attrs = %{params: %{}, scheduled_at: today}
551 {:ok, _} = ScheduledActivity.create(user, attrs)
552 {:ok, _} = ScheduledActivity.create(user, attrs)
556 |> put_req_header("content-type", "application/json")
557 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
559 assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
562 test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
564 NaiveDateTime.utc_now()
565 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
566 |> NaiveDateTime.to_iso8601()
570 NaiveDateTime.utc_now()
571 |> NaiveDateTime.add(:timer.hours(36), :millisecond)
572 |> NaiveDateTime.to_iso8601()
575 attrs = %{params: %{}, scheduled_at: today}
576 {:ok, _} = ScheduledActivity.create(user, attrs)
577 {:ok, _} = ScheduledActivity.create(user, attrs)
578 {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
582 |> put_req_header("content-type", "application/json")
583 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
585 assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
589 describe "posting polls" do
590 setup do: oauth_access(["write:statuses"])
592 test "posting a poll", %{conn: conn} do
593 time = NaiveDateTime.utc_now()
597 |> put_req_header("content-type", "application/json")
598 |> post("/api/v1/statuses", %{
599 "status" => "Who is the #bestgrill?",
601 "options" => ["Rei", "Asuka", "Misato"],
606 response = json_response_and_validate_schema(conn, 200)
608 assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
609 title in ["Rei", "Asuka", "Misato"]
612 assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
613 assert response["poll"]["expired"] == false
615 question = Object.get_by_id(response["poll"]["id"])
617 # closed contains utc timezone
618 assert question.data["closed"] =~ "Z"
621 test "option limit is enforced", %{conn: conn} do
622 limit = Config.get([:instance, :poll_limits, :max_options])
626 |> put_req_header("content-type", "application/json")
627 |> post("/api/v1/statuses", %{
629 "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
632 %{"error" => error} = json_response_and_validate_schema(conn, 422)
633 assert error == "Poll can't contain more than #{limit} options"
636 test "option character limit is enforced", %{conn: conn} do
637 limit = Config.get([:instance, :poll_limits, :max_option_chars])
641 |> put_req_header("content-type", "application/json")
642 |> post("/api/v1/statuses", %{
645 "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
650 %{"error" => error} = json_response_and_validate_schema(conn, 422)
651 assert error == "Poll options cannot be longer than #{limit} characters each"
654 test "minimal date limit is enforced", %{conn: conn} do
655 limit = Config.get([:instance, :poll_limits, :min_expiration])
659 |> put_req_header("content-type", "application/json")
660 |> post("/api/v1/statuses", %{
661 "status" => "imagine arbitrary limits",
663 "options" => ["this post was made by pleroma gang"],
664 "expires_in" => limit - 1
668 %{"error" => error} = json_response_and_validate_schema(conn, 422)
669 assert error == "Expiration date is too soon"
672 test "maximum date limit is enforced", %{conn: conn} do
673 limit = Config.get([:instance, :poll_limits, :max_expiration])
677 |> put_req_header("content-type", "application/json")
678 |> post("/api/v1/statuses", %{
679 "status" => "imagine arbitrary limits",
681 "options" => ["this post was made by pleroma gang"],
682 "expires_in" => limit + 1
686 %{"error" => error} = json_response_and_validate_schema(conn, 422)
687 assert error == "Expiration date is too far in the future"
690 test "scheduled poll", %{conn: conn} do
691 clear_config([ScheduledActivity, :enabled], true)
694 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
695 |> NaiveDateTime.to_iso8601()
698 %{"id" => scheduled_id} =
700 |> put_req_header("content-type", "application/json")
701 |> post("/api/v1/statuses", %{
702 "status" => "very cool poll",
704 "options" => ~w(a b c),
707 "scheduled_at" => scheduled_at
709 |> json_response_and_validate_schema(200)
711 assert {:ok, %{id: activity_id}} =
712 perform_job(ScheduledActivityWorker, %{
713 activity_id: scheduled_id
716 refute_enqueued(worker: ScheduledActivityWorker)
720 |> Repo.get(activity_id)
721 |> Object.normalize()
723 assert object.data["content"] == "very cool poll"
724 assert object.data["type"] == "Question"
725 assert length(object.data["oneOf"]) == 3
729 test "get a status" do
730 %{conn: conn} = oauth_access(["read:statuses"])
731 activity = insert(:note_activity)
733 conn = get(conn, "/api/v1/statuses/#{activity.id}")
735 assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
736 assert id == to_string(activity.id)
739 defp local_and_remote_activities do
740 local = insert(:note_activity)
741 remote = insert(:note_activity, local: false)
742 {:ok, local: local, remote: remote}
745 describe "status with restrict unauthenticated activities for local and remote" do
746 setup do: local_and_remote_activities()
748 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
750 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
752 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
753 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
755 assert json_response_and_validate_schema(res_conn, :not_found) == %{
756 "error" => "Record not found"
759 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
761 assert json_response_and_validate_schema(res_conn, :not_found) == %{
762 "error" => "Record not found"
766 test "if user is authenticated", %{local: local, remote: remote} do
767 %{conn: conn} = oauth_access(["read"])
768 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
769 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
771 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
772 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
776 describe "status with restrict unauthenticated activities for local" do
777 setup do: local_and_remote_activities()
779 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
781 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
782 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
784 assert json_response_and_validate_schema(res_conn, :not_found) == %{
785 "error" => "Record not found"
788 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
789 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
792 test "if user is authenticated", %{local: local, remote: remote} do
793 %{conn: conn} = oauth_access(["read"])
794 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
795 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
797 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
798 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
802 describe "status with restrict unauthenticated activities for remote" do
803 setup do: local_and_remote_activities()
805 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
807 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
808 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
809 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
811 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
813 assert json_response_and_validate_schema(res_conn, :not_found) == %{
814 "error" => "Record not found"
818 test "if user is authenticated", %{local: local, remote: remote} do
819 %{conn: conn} = oauth_access(["read"])
820 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
821 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
823 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
824 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
828 test "getting a status that doesn't exist returns 404" do
829 %{conn: conn} = oauth_access(["read:statuses"])
830 activity = insert(:note_activity)
832 conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
834 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
837 test "get a direct status" do
838 %{user: user, conn: conn} = oauth_access(["read:statuses"])
839 other_user = insert(:user)
842 CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
846 |> assign(:user, user)
847 |> get("/api/v1/statuses/#{activity.id}")
849 [participation] = Participation.for_user(user)
851 res = json_response_and_validate_schema(conn, 200)
852 assert res["pleroma"]["direct_conversation_id"] == participation.id
855 test "get statuses by IDs" do
856 %{conn: conn} = oauth_access(["read:statuses"])
857 %{id: id1} = insert(:note_activity)
858 %{id: id2} = insert(:note_activity)
860 query_string = "ids[]=#{id1}&ids[]=#{id2}"
861 conn = get(conn, "/api/v1/statuses/?#{query_string}")
863 assert [%{"id" => ^id1}, %{"id" => ^id2}] =
864 Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
867 describe "getting statuses by ids with restricted unauthenticated for local and remote" do
868 setup do: local_and_remote_activities()
870 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
872 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
874 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
875 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
877 assert json_response_and_validate_schema(res_conn, 200) == []
880 test "if user is authenticated", %{local: local, remote: remote} do
881 %{conn: conn} = oauth_access(["read"])
883 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
885 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
889 describe "getting statuses by ids with restricted unauthenticated for local" do
890 setup do: local_and_remote_activities()
892 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
894 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
895 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
897 remote_id = remote.id
898 assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
901 test "if user is authenticated", %{local: local, remote: remote} do
902 %{conn: conn} = oauth_access(["read"])
904 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
906 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
910 describe "getting statuses by ids with restricted unauthenticated for remote" do
911 setup do: local_and_remote_activities()
913 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
915 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
916 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
919 assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
922 test "if user is authenticated", %{local: local, remote: remote} do
923 %{conn: conn} = oauth_access(["read"])
925 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
927 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
931 describe "deleting a status" do
932 test "when you created it" do
933 %{user: author, conn: conn} = oauth_access(["write:statuses"])
934 activity = insert(:note_activity, user: author)
935 object = Object.normalize(activity, fetch: false)
937 content = object.data["content"]
938 source = object.data["source"]
942 |> assign(:user, author)
943 |> delete("/api/v1/statuses/#{activity.id}")
944 |> json_response_and_validate_schema(200)
946 assert match?(%{"content" => ^content, "text" => ^source}, result)
948 refute Activity.get_by_id(activity.id)
951 test "when it doesn't exist" do
952 %{user: author, conn: conn} = oauth_access(["write:statuses"])
953 activity = insert(:note_activity, user: author)
957 |> assign(:user, author)
958 |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
960 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
963 test "when you didn't create it" do
964 %{conn: conn} = oauth_access(["write:statuses"])
965 activity = insert(:note_activity)
967 conn = delete(conn, "/api/v1/statuses/#{activity.id}")
969 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
971 assert Activity.get_by_id(activity.id) == activity
974 test "when you're privileged to", %{conn: conn} do
975 clear_config([:instance, :moderator_privileges], [:messages_delete])
976 activity = insert(:note_activity)
977 user = insert(:user, is_moderator: true)
981 |> assign(:user, user)
982 |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
983 |> delete("/api/v1/statuses/#{activity.id}")
985 assert %{} = json_response_and_validate_schema(res_conn, 200)
987 assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() ==
988 "@#{user.nickname} deleted status ##{activity.id}"
990 refute Activity.get_by_id(activity.id)
994 describe "reblogging" do
995 setup do: oauth_access(["write:statuses"])
997 test "reblogs and returns the reblogged status", %{conn: conn} do
998 activity = insert(:note_activity)
1002 |> put_req_header("content-type", "application/json")
1003 |> post("/api/v1/statuses/#{activity.id}/reblog")
1006 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1008 } = json_response_and_validate_schema(conn, 200)
1010 assert to_string(activity.id) == id
1013 test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
1014 activity = insert(:note_activity)
1018 |> put_req_header("content-type", "application/json")
1019 |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
1021 assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
1024 test "reblogs privately and returns the reblogged status", %{conn: conn} do
1025 activity = insert(:note_activity)
1029 |> put_req_header("content-type", "application/json")
1031 "/api/v1/statuses/#{activity.id}/reblog",
1032 %{"visibility" => "private"}
1036 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1037 "reblogged" => true,
1038 "visibility" => "private"
1039 } = json_response_and_validate_schema(conn, 200)
1041 assert to_string(activity.id) == id
1044 test "reblogged status for another user" do
1045 activity = insert(:note_activity)
1046 user1 = insert(:user)
1047 user2 = insert(:user)
1048 user3 = insert(:user)
1049 {:ok, _} = CommonAPI.favorite(user2, activity.id)
1050 {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
1051 {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
1052 {:ok, _} = CommonAPI.repeat(activity.id, user2)
1056 |> assign(:user, user3)
1057 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1058 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1061 "reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
1062 "reblogged" => false,
1063 "favourited" => false,
1064 "bookmarked" => false
1065 } = json_response_and_validate_schema(conn_res, 200)
1069 |> assign(:user, user2)
1070 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
1071 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1074 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
1075 "reblogged" => true,
1076 "favourited" => true,
1077 "bookmarked" => true
1078 } = json_response_and_validate_schema(conn_res, 200)
1080 assert to_string(activity.id) == id
1083 test "author can reblog own private status", %{conn: conn, user: user} do
1084 {:ok, activity} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
1088 |> put_req_header("content-type", "application/json")
1089 |> post("/api/v1/statuses/#{activity.id}/reblog")
1092 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1093 "reblogged" => true,
1094 "visibility" => "private"
1095 } = json_response_and_validate_schema(conn, 200)
1097 assert to_string(activity.id) == id
1101 describe "unreblogging" do
1102 setup do: oauth_access(["write:statuses"])
1104 test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
1105 activity = insert(:note_activity)
1107 {:ok, _} = CommonAPI.repeat(activity.id, user)
1111 |> put_req_header("content-type", "application/json")
1112 |> post("/api/v1/statuses/#{activity.id}/unreblog")
1114 assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
1115 json_response_and_validate_schema(conn, 200)
1117 assert to_string(activity.id) == id
1120 test "returns 404 error when activity does not exist", %{conn: conn} do
1123 |> put_req_header("content-type", "application/json")
1124 |> post("/api/v1/statuses/foo/unreblog")
1126 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1130 describe "favoriting" do
1131 setup do: oauth_access(["write:favourites"])
1133 test "favs a status and returns it", %{conn: conn} do
1134 activity = insert(:note_activity)
1138 |> put_req_header("content-type", "application/json")
1139 |> post("/api/v1/statuses/#{activity.id}/favourite")
1141 assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
1142 json_response_and_validate_schema(conn, 200)
1144 assert to_string(activity.id) == id
1147 test "favoriting twice will just return 200", %{conn: conn} do
1148 activity = insert(:note_activity)
1151 |> put_req_header("content-type", "application/json")
1152 |> post("/api/v1/statuses/#{activity.id}/favourite")
1155 |> put_req_header("content-type", "application/json")
1156 |> post("/api/v1/statuses/#{activity.id}/favourite")
1157 |> json_response_and_validate_schema(200)
1160 test "returns 404 error for a wrong id", %{conn: conn} do
1163 |> put_req_header("content-type", "application/json")
1164 |> post("/api/v1/statuses/1/favourite")
1166 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1170 describe "unfavoriting" do
1171 setup do: oauth_access(["write:favourites"])
1173 test "unfavorites a status and returns it", %{user: user, conn: conn} do
1174 activity = insert(:note_activity)
1176 {:ok, _} = CommonAPI.favorite(user, activity.id)
1180 |> put_req_header("content-type", "application/json")
1181 |> post("/api/v1/statuses/#{activity.id}/unfavourite")
1183 assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
1184 json_response_and_validate_schema(conn, 200)
1186 assert to_string(activity.id) == id
1189 test "returns 404 error for a wrong id", %{conn: conn} do
1192 |> put_req_header("content-type", "application/json")
1193 |> post("/api/v1/statuses/1/unfavourite")
1195 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1199 describe "pinned statuses" do
1200 setup do: oauth_access(["write:accounts"])
1202 setup %{user: user} do
1203 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
1205 %{activity: activity}
1208 setup do: clear_config([:instance, :max_pinned_statuses], 1)
1210 test "pin status", %{conn: conn, user: user, activity: activity} do
1213 assert %{"id" => ^id, "pinned" => true} =
1215 |> put_req_header("content-type", "application/json")
1216 |> post("/api/v1/statuses/#{activity.id}/pin")
1217 |> json_response_and_validate_schema(200)
1219 assert [%{"id" => ^id, "pinned" => true}] =
1221 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1222 |> json_response_and_validate_schema(200)
1225 test "non authenticated user", %{activity: activity} do
1227 |> put_req_header("content-type", "application/json")
1228 |> post("/api/v1/statuses/#{activity.id}/pin")
1229 |> json_response(403) == %{"error" => "Invalid credentials."}
1232 test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
1233 {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
1237 |> put_req_header("content-type", "application/json")
1238 |> post("/api/v1/statuses/#{dm.id}/pin")
1240 assert json_response_and_validate_schema(conn, 422) == %{
1241 "error" => "Non-public status cannot be pinned"
1245 test "pin by another user", %{activity: activity} do
1246 %{conn: conn} = oauth_access(["write:accounts"])
1249 |> put_req_header("content-type", "application/json")
1250 |> post("/api/v1/statuses/#{activity.id}/pin")
1251 |> json_response(422) == %{"error" => "Someone else's status cannot be pinned"}
1254 test "unpin status", %{conn: conn, user: user, activity: activity} do
1255 {:ok, _} = CommonAPI.pin(activity.id, user)
1256 user = refresh_record(user)
1258 id_str = to_string(activity.id)
1260 assert %{"id" => ^id_str, "pinned" => false} =
1262 |> assign(:user, user)
1263 |> post("/api/v1/statuses/#{activity.id}/unpin")
1264 |> json_response_and_validate_schema(200)
1268 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1269 |> json_response_and_validate_schema(200)
1272 test "/unpin: returns 404 error when activity doesn't exist", %{conn: conn} do
1274 |> put_req_header("content-type", "application/json")
1275 |> post("/api/v1/statuses/1/unpin")
1276 |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
1279 test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
1280 {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
1282 id_str_one = to_string(activity_one.id)
1284 assert %{"id" => ^id_str_one, "pinned" => true} =
1286 |> put_req_header("content-type", "application/json")
1287 |> post("/api/v1/statuses/#{id_str_one}/pin")
1288 |> json_response_and_validate_schema(200)
1290 user = refresh_record(user)
1292 assert %{"error" => "You have already pinned the maximum number of statuses"} =
1294 |> assign(:user, user)
1295 |> post("/api/v1/statuses/#{activity_two.id}/pin")
1296 |> json_response_and_validate_schema(400)
1299 test "on pin removes deletion job, on unpin reschedule deletion" do
1300 %{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
1301 expires_in = 2 * 60 * 60
1303 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
1305 assert %{"id" => id} =
1307 |> put_req_header("content-type", "application/json")
1308 |> post("api/v1/statuses", %{
1309 "status" => "oolong",
1310 "expires_in" => expires_in
1312 |> json_response_and_validate_schema(200)
1315 worker: Pleroma.Workers.PurgeExpiredActivity,
1316 args: %{activity_id: id},
1317 scheduled_at: expires_at
1320 assert %{"id" => ^id, "pinned" => true} =
1322 |> put_req_header("content-type", "application/json")
1323 |> post("/api/v1/statuses/#{id}/pin")
1324 |> json_response_and_validate_schema(200)
1327 worker: Pleroma.Workers.PurgeExpiredActivity,
1328 args: %{activity_id: id},
1329 scheduled_at: expires_at
1332 assert %{"id" => ^id, "pinned" => false} =
1334 |> put_req_header("content-type", "application/json")
1335 |> post("/api/v1/statuses/#{id}/unpin")
1336 |> json_response_and_validate_schema(200)
1339 worker: Pleroma.Workers.PurgeExpiredActivity,
1340 args: %{activity_id: id},
1341 scheduled_at: expires_at
1348 clear_config([:rich_media, :enabled], true)
1350 oauth_access(["read:statuses"])
1353 test "returns rich-media card", %{conn: conn, user: user} do
1354 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
1356 {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"})
1359 "image" => "http://ia.media-imdb.com/images/rock.jpg",
1360 "provider_name" => "example.com",
1361 "provider_url" => "https://example.com",
1362 "title" => "The Rock",
1364 "url" => "https://example.com/ogp",
1366 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
1369 "image" => "http://ia.media-imdb.com/images/rock.jpg",
1370 "title" => "The Rock",
1371 "type" => "video.movie",
1372 "url" => "https://example.com/ogp",
1374 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
1381 |> get("/api/v1/statuses/#{activity.id}/card")
1382 |> json_response_and_validate_schema(200)
1384 assert response == card_data
1386 # works with private posts
1388 CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"})
1392 |> get("/api/v1/statuses/#{activity.id}/card")
1393 |> json_response_and_validate_schema(200)
1395 assert response_two == card_data
1398 test "replaces missing description with an empty string", %{conn: conn, user: user} do
1399 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
1401 {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"})
1405 |> get("/api/v1/statuses/#{activity.id}/card")
1406 |> json_response_and_validate_schema(:ok)
1408 assert response == %{
1410 "title" => "Pleroma",
1411 "description" => "",
1413 "provider_name" => "example.com",
1414 "provider_url" => "https://example.com",
1415 "url" => "https://example.com/ogp-missing-data",
1418 "title" => "Pleroma",
1419 "type" => "website",
1420 "url" => "https://example.com/ogp-missing-data"
1428 bookmarks_uri = "/api/v1/bookmarks"
1430 %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
1431 author = insert(:user)
1433 {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
1434 {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
1438 |> put_req_header("content-type", "application/json")
1439 |> post("/api/v1/statuses/#{activity1.id}/bookmark")
1441 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
1445 |> put_req_header("content-type", "application/json")
1446 |> post("/api/v1/statuses/#{activity2.id}/bookmark")
1448 assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
1450 bookmarks = get(conn, bookmarks_uri)
1453 json_response_and_validate_schema(response2, 200),
1454 json_response_and_validate_schema(response1, 200)
1456 json_response_and_validate_schema(bookmarks, 200)
1460 |> put_req_header("content-type", "application/json")
1461 |> post("/api/v1/statuses/#{activity1.id}/unbookmark")
1463 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
1465 bookmarks = get(conn, bookmarks_uri)
1467 assert [json_response_and_validate_schema(response2, 200)] ==
1468 json_response_and_validate_schema(bookmarks, 200)
1471 describe "conversation muting" do
1472 setup do: oauth_access(["write:mutes"])
1475 post_user = insert(:user)
1476 {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
1477 %{activity: activity}
1480 test "mute conversation", %{conn: conn, activity: activity} do
1481 id_str = to_string(activity.id)
1483 assert %{"id" => ^id_str, "muted" => true} =
1485 |> put_req_header("content-type", "application/json")
1486 |> post("/api/v1/statuses/#{activity.id}/mute")
1487 |> json_response_and_validate_schema(200)
1490 test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
1491 {:ok, _} = CommonAPI.add_mute(user, activity)
1495 |> put_req_header("content-type", "application/json")
1496 |> post("/api/v1/statuses/#{activity.id}/mute")
1498 assert json_response_and_validate_schema(conn, 400) == %{
1499 "error" => "conversation is already muted"
1503 test "unmute conversation", %{conn: conn, user: user, activity: activity} do
1504 {:ok, _} = CommonAPI.add_mute(user, activity)
1506 id_str = to_string(activity.id)
1508 assert %{"id" => ^id_str, "muted" => false} =
1510 # |> assign(:user, user)
1511 |> post("/api/v1/statuses/#{activity.id}/unmute")
1512 |> json_response_and_validate_schema(200)
1516 test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
1517 user1 = insert(:user)
1518 user2 = insert(:user)
1519 user3 = insert(:user)
1521 {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
1523 # Reply to status from another user
1526 |> assign(:user, user2)
1527 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
1528 |> put_req_header("content-type", "application/json")
1529 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
1531 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
1533 activity = Activity.get_by_id_with_object(id)
1535 assert Object.normalize(activity, fetch: false).data["inReplyTo"] ==
1536 Object.normalize(replied_to, fetch: false).data["id"]
1538 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
1540 # Reblog from the third user
1543 |> assign(:user, user3)
1544 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
1545 |> put_req_header("content-type", "application/json")
1546 |> post("/api/v1/statuses/#{activity.id}/reblog")
1548 assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
1549 json_response_and_validate_schema(conn2, 200)
1551 assert to_string(activity.id) == id
1553 # Getting third user status
1556 |> assign(:user, user3)
1557 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1558 |> get("api/v1/timelines/home")
1560 [reblogged_activity] = json_response_and_validate_schema(conn3, 200)
1562 assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
1564 replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
1565 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
1568 describe "GET /api/v1/statuses/:id/favourited_by" do
1569 setup do: oauth_access(["read:accounts"])
1571 setup %{user: user} do
1572 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1574 %{activity: activity}
1577 test "returns users who have favorited the status", %{conn: conn, activity: activity} do
1578 other_user = insert(:user)
1579 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1583 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1584 |> json_response_and_validate_schema(:ok)
1586 [%{"id" => id}] = response
1588 assert id == other_user.id
1591 test "returns empty array when status has not been favorited yet", %{
1597 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1598 |> json_response_and_validate_schema(:ok)
1600 assert Enum.empty?(response)
1603 test "does not return users who have favorited the status but are blocked", %{
1604 conn: %{assigns: %{user: user}} = conn,
1607 other_user = insert(:user)
1608 {:ok, _user_relationship} = User.block(user, other_user)
1610 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1614 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1615 |> json_response_and_validate_schema(:ok)
1617 assert Enum.empty?(response)
1620 test "does not fail on an unauthenticated request", %{activity: activity} do
1621 other_user = insert(:user)
1622 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1626 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1627 |> json_response_and_validate_schema(:ok)
1629 [%{"id" => id}] = response
1630 assert id == other_user.id
1633 test "requires authentication for private posts", %{user: user} do
1634 other_user = insert(:user)
1637 CommonAPI.post(user, %{
1638 status: "@#{other_user.nickname} wanna get some #cofe together?",
1639 visibility: "direct"
1642 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1644 favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
1647 |> get(favourited_by_url)
1648 |> json_response_and_validate_schema(404)
1652 |> assign(:user, other_user)
1653 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1656 |> assign(:token, nil)
1657 |> get(favourited_by_url)
1658 |> json_response_and_validate_schema(404)
1662 |> get(favourited_by_url)
1663 |> json_response_and_validate_schema(200)
1665 [%{"id" => id}] = response
1666 assert id == other_user.id
1669 test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
1670 clear_config([:instance, :show_reactions], false)
1672 other_user = insert(:user)
1673 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1677 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1678 |> json_response_and_validate_schema(:ok)
1680 assert Enum.empty?(response)
1684 describe "GET /api/v1/statuses/:id/reblogged_by" do
1685 setup do: oauth_access(["read:accounts"])
1687 setup %{user: user} do
1688 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1690 %{activity: activity}
1693 test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
1694 other_user = insert(:user)
1695 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1699 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1700 |> json_response_and_validate_schema(:ok)
1702 [%{"id" => id}] = response
1704 assert id == other_user.id
1707 test "returns empty array when status has not been reblogged yet", %{
1713 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1714 |> json_response_and_validate_schema(:ok)
1716 assert Enum.empty?(response)
1719 test "does not return users who have reblogged the status but are blocked", %{
1720 conn: %{assigns: %{user: user}} = conn,
1723 other_user = insert(:user)
1724 {:ok, _user_relationship} = User.block(user, other_user)
1726 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1730 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1731 |> json_response_and_validate_schema(:ok)
1733 assert Enum.empty?(response)
1736 test "does not return users who have reblogged the status privately", %{
1739 other_user = insert(:user)
1740 {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
1742 {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
1746 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1747 |> json_response_and_validate_schema(:ok)
1749 assert Enum.empty?(response)
1752 test "does not fail on an unauthenticated request", %{activity: activity} do
1753 other_user = insert(:user)
1754 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1758 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1759 |> json_response_and_validate_schema(:ok)
1761 [%{"id" => id}] = response
1762 assert id == other_user.id
1765 test "requires authentication for private posts", %{user: user} do
1766 other_user = insert(:user)
1769 CommonAPI.post(user, %{
1770 status: "@#{other_user.nickname} wanna get some #cofe together?",
1771 visibility: "direct"
1775 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1776 |> json_response_and_validate_schema(404)
1780 |> assign(:user, other_user)
1781 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1782 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1783 |> json_response_and_validate_schema(200)
1785 assert [] == response
1790 user = insert(:user)
1792 {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
1793 {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
1794 {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
1795 {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
1796 {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
1800 |> get("/api/v1/statuses/#{id3}/context")
1801 |> json_response_and_validate_schema(:ok)
1804 "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
1805 "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
1809 test "favorites paginate correctly" do
1810 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1811 other_user = insert(:user)
1812 {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
1813 {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
1814 {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
1816 {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
1817 {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
1818 {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
1822 |> get("/api/v1/favourites?limit=1")
1824 assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
1825 assert post_id == second_post.id
1827 # Using the header for pagination works correctly
1828 [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
1829 [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next)
1831 assert max_id == third_favorite.id
1835 |> get("/api/v1/favourites?max_id=#{max_id}")
1837 assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
1838 json_response_and_validate_schema(result, 200)
1840 assert first_post_id == first_post.id
1841 assert third_post_id == third_post.id
1844 test "returns the favorites of a user" do
1845 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1846 other_user = insert(:user)
1848 {:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
1849 {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
1851 {:ok, last_like} = CommonAPI.favorite(user, activity.id)
1853 first_conn = get(conn, "/api/v1/favourites")
1855 assert [status] = json_response_and_validate_schema(first_conn, 200)
1856 assert status["id"] == to_string(activity.id)
1858 assert [{"link", _link_header}] =
1859 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
1861 # Honours query params
1862 {:ok, second_activity} =
1863 CommonAPI.post(other_user, %{
1864 status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
1867 {:ok, _} = CommonAPI.favorite(user, second_activity.id)
1869 second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
1871 assert [second_status] = json_response_and_validate_schema(second_conn, 200)
1872 assert second_status["id"] == to_string(second_activity.id)
1874 third_conn = get(conn, "/api/v1/favourites?limit=0")
1876 assert [] = json_response_and_validate_schema(third_conn, 200)
1879 test "expires_at is nil for another user" do
1880 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1881 expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
1882 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
1884 assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
1886 |> get("/api/v1/statuses/#{activity.id}")
1887 |> json_response_and_validate_schema(:ok)
1889 {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
1890 assert DateTime.diff(expires_at, a_expires_at) == 0
1892 %{conn: conn} = oauth_access(["read:statuses"])
1894 assert %{"pleroma" => %{"expires_at" => nil}} =
1896 |> get("/api/v1/statuses/#{activity.id}")
1897 |> json_response_and_validate_schema(:ok)
1900 describe "local-only statuses" do
1901 test "posting a local only status" do
1902 %{user: _user, conn: conn} = oauth_access(["write:statuses"])
1906 |> put_req_header("content-type", "application/json")
1907 |> post("/api/v1/statuses", %{
1909 "visibility" => "local"
1912 local = Utils.as_local_public()
1914 assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
1915 json_response_and_validate_schema(conn_one, 200)
1917 assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
1920 test "other users can read local-only posts" do
1921 user = insert(:user)
1922 %{user: _reader, conn: conn} = oauth_access(["read:statuses"])
1924 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1928 |> get("/api/v1/statuses/#{activity.id}")
1929 |> json_response_and_validate_schema(:ok)
1931 assert received["id"] == activity.id
1934 test "anonymous users cannot see local-only posts" do
1935 user = insert(:user)
1937 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
1941 |> get("/api/v1/statuses/#{activity.id}")
1942 |> json_response_and_validate_schema(:not_found)
1946 describe "muted reactions" do
1948 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1950 other_user = insert(:user)
1951 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1953 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
1954 User.mute(user, other_user)
1958 |> get("/api/v1/statuses/?ids[]=#{activity.id}")
1959 |> json_response_and_validate_schema(200)
1964 "emoji_reactions" => []
1971 |> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
1972 |> json_response_and_validate_schema(200)
1977 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
1984 # %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
1985 %{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
1987 other_user = insert(:user)
1988 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1990 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
1991 User.mute(user, other_user)
1995 |> get("/api/v1/statuses/#{activity.id}")
1996 |> json_response_and_validate_schema(200)
2000 "emoji_reactions" => []
2006 |> get("/api/v1/statuses/#{activity.id}?with_muted=true")
2007 |> json_response_and_validate_schema(200)
2011 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
2017 describe "get status history" do
2019 %{conn: build_conn()}
2022 test "unedited post", %{conn: conn} do
2023 activity = insert(:note_activity)
2025 conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
2027 assert [_] = json_response_and_validate_schema(conn, 200)
2030 test "edited post", %{conn: conn} do
2035 "formerRepresentations" => %{
2036 "type" => "OrderedCollection",
2040 "content" => "mew mew 2",
2041 "summary" => "title 2"
2045 "content" => "mew mew 1",
2046 "summary" => "title 1"
2054 activity = insert(:note_activity, note: note)
2056 conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
2058 assert [%{"spoiler_text" => "title 1"}, %{"spoiler_text" => "title 2"}, _] =
2059 json_response_and_validate_schema(conn, 200)
2063 describe "get status source" do
2065 %{conn: build_conn()}
2068 test "it returns the source", %{conn: conn} do
2069 user = insert(:user)
2071 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2073 conn = get(conn, "/api/v1/statuses/#{activity.id}/source")
2077 assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
2078 json_response_and_validate_schema(conn, 200)
2082 describe "update status" do
2084 oauth_access(["write:statuses"])
2087 test "it updates the status" do
2088 %{conn: conn, user: user} = oauth_access(["write:statuses", "read:statuses"])
2090 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2093 |> get("/api/v1/statuses/#{activity.id}")
2094 |> json_response_and_validate_schema(200)
2098 |> put_req_header("content-type", "application/json")
2099 |> put("/api/v1/statuses/#{activity.id}", %{
2100 "status" => "edited",
2101 "spoiler_text" => "lol"
2103 |> json_response_and_validate_schema(200)
2105 assert response["content"] == "edited"
2106 assert response["spoiler_text"] == "lol"
2110 |> get("/api/v1/statuses/#{activity.id}")
2111 |> json_response_and_validate_schema(200)
2113 assert response["content"] == "edited"
2114 assert response["spoiler_text"] == "lol"
2117 test "it updates the attachments", %{conn: conn, user: user} do
2118 attachment = insert(:attachment, user: user)
2119 attachment_id = to_string(attachment.id)
2121 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2125 |> put_req_header("content-type", "application/json")
2126 |> put("/api/v1/statuses/#{activity.id}", %{
2127 "status" => "mew mew #abc",
2128 "spoiler_text" => "#def",
2129 "media_ids" => [attachment_id]
2131 |> json_response_and_validate_schema(200)
2133 assert [%{"id" => ^attachment_id}] = response["media_attachments"]
2136 test "it does not update visibility", %{conn: conn, user: user} do
2138 CommonAPI.post(user, %{
2139 status: "mew mew #abc",
2140 spoiler_text: "#def",
2141 visibility: "private"
2146 |> put_req_header("content-type", "application/json")
2147 |> put("/api/v1/statuses/#{activity.id}", %{
2148 "status" => "edited",
2149 "spoiler_text" => "lol"
2151 |> json_response_and_validate_schema(200)
2153 assert response["visibility"] == "private"
2156 test "it refuses to update when original post is not by the user", %{conn: conn} do
2157 another_user = insert(:user)
2160 CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
2163 |> put_req_header("content-type", "application/json")
2164 |> put("/api/v1/statuses/#{activity.id}", %{
2165 "status" => "edited",
2166 "spoiler_text" => "lol"
2168 |> json_response_and_validate_schema(:forbidden)
2171 test "it returns 404 if the user cannot see the post", %{conn: conn} do
2172 another_user = insert(:user)
2175 CommonAPI.post(another_user, %{
2176 status: "mew mew #abc",
2177 spoiler_text: "#def",
2178 visibility: "private"
2182 |> put_req_header("content-type", "application/json")
2183 |> put("/api/v1/statuses/#{activity.id}", %{
2184 "status" => "edited",
2185 "spoiler_text" => "lol"
2187 |> json_response_and_validate_schema(:not_found)