1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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
9 alias Pleroma.ActivityExpiration
11 alias Pleroma.Conversation.Participation
14 alias Pleroma.ScheduledActivity
15 alias Pleroma.Tests.ObanHelpers
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.CommonAPI
20 import Pleroma.Factory
22 setup do: clear_config([:instance, :federating])
23 setup do: clear_config([:instance, :allow_relay])
24 setup do: clear_config([:rich_media, :enabled])
26 describe "posting statuses" do
27 setup do: oauth_access(["write:statuses"])
29 test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
30 Pleroma.Config.put([:instance, :federating], true)
31 Pleroma.Config.get([:instance, :allow_relay], true)
35 |> post("api/v1/statuses", %{
36 "content_type" => "text/plain",
37 "source" => "Pleroma FE",
38 "status" => "Hello world",
39 "visibility" => "public"
43 assert response["reblogs_count"] == 0
44 ObanHelpers.perform_all()
48 |> get("api/v1/statuses/#{response["id"]}", %{})
51 assert response["reblogs_count"] == 0
54 test "posting a status", %{conn: conn} do
55 idempotency_key = "Pikachu rocks!"
59 |> put_req_header("idempotency-key", idempotency_key)
60 |> post("/api/v1/statuses", %{
62 "spoiler_text" => "2hu",
63 "sensitive" => "false"
66 {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
68 assert ttl > :timer.seconds(6 * 60 * 60 - 1)
70 assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
71 json_response(conn_one, 200)
73 assert Activity.get_by_id(id)
77 |> put_req_header("idempotency-key", idempotency_key)
78 |> post("/api/v1/statuses", %{
80 "spoiler_text" => "2hu",
81 "sensitive" => "false"
84 assert %{"id" => second_id} = json_response(conn_two, 200)
85 assert id == second_id
89 |> post("/api/v1/statuses", %{
91 "spoiler_text" => "2hu",
92 "sensitive" => "false"
95 assert %{"id" => third_id} = json_response(conn_three, 200)
98 # An activity that will expire:
100 expires_in = 120 * 60
104 |> post("api/v1/statuses", %{
105 "status" => "oolong",
106 "expires_in" => expires_in
109 assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200)
110 assert activity = Activity.get_by_id(fourth_id)
111 assert expiration = ActivityExpiration.get_by_activity_id(fourth_id)
113 estimated_expires_at =
114 NaiveDateTime.utc_now()
115 |> NaiveDateTime.add(expires_in)
116 |> NaiveDateTime.truncate(:second)
118 # This assert will fail if the test takes longer than a minute. I sure hope it never does:
119 assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60
121 assert fourth_response["pleroma"]["expires_at"] ==
122 NaiveDateTime.to_iso8601(expiration.scheduled_at)
125 test "it fails to create a status if `expires_in` is less or equal than an hour", %{
131 assert %{"error" => "Expiry date is too soon"} =
133 |> post("api/v1/statuses", %{
134 "status" => "oolong",
135 "expires_in" => expires_in
137 |> json_response(422)
142 assert %{"error" => "Expiry date is too soon"} =
144 |> post("api/v1/statuses", %{
145 "status" => "oolong",
146 "expires_in" => expires_in
148 |> json_response(422)
151 test "posting an undefined status with an attachment", %{user: user, conn: conn} do
153 content_type: "image/jpg",
154 path: Path.absname("test/fixtures/image.jpg"),
155 filename: "an_image.jpg"
158 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
161 post(conn, "/api/v1/statuses", %{
162 "media_ids" => [to_string(upload.id)]
165 assert json_response(conn, 200)
168 test "replying to a status", %{user: user, conn: conn} do
169 {:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"})
173 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
175 assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
177 activity = Activity.get_by_id(id)
179 assert activity.data["context"] == replied_to.data["context"]
180 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
183 test "replying to a direct message with visibility other than direct", %{
187 {:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
189 Enum.each(["public", "private", "unlisted"], fn visibility ->
192 |> post("/api/v1/statuses", %{
193 "status" => "@#{user.nickname} hey",
194 "in_reply_to_id" => replied_to.id,
195 "visibility" => visibility
198 assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"}
202 test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
203 conn = post(conn, "/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
205 assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
206 assert Activity.get_by_id(id)
209 test "posting a sensitive status", %{conn: conn} do
210 conn = post(conn, "/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
212 assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
213 assert Activity.get_by_id(id)
216 test "posting a fake status", %{conn: conn} do
218 post(conn, "/api/v1/statuses", %{
220 "\"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"
223 real_status = json_response(real_conn, 200)
226 assert Object.get_by_ap_id(real_status["uri"])
230 |> Map.put("id", nil)
231 |> Map.put("url", nil)
232 |> Map.put("uri", nil)
233 |> Map.put("created_at", nil)
234 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
237 post(conn, "/api/v1/statuses", %{
239 "\"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",
243 fake_status = json_response(fake_conn, 200)
246 refute Object.get_by_ap_id(fake_status["uri"])
250 |> Map.put("id", nil)
251 |> Map.put("url", nil)
252 |> Map.put("uri", nil)
253 |> Map.put("created_at", nil)
254 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
256 assert real_status == fake_status
259 test "posting a status with OGP link preview", %{conn: conn} do
260 Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
261 Config.put([:rich_media, :enabled], true)
264 post(conn, "/api/v1/statuses", %{
265 "status" => "https://example.com/ogp"
268 assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
269 assert Activity.get_by_id(id)
272 test "posting a direct status", %{conn: conn} do
273 user2 = insert(:user)
274 content = "direct cofe @#{user2.nickname}"
276 conn = post(conn, "api/v1/statuses", %{"status" => content, "visibility" => "direct"})
278 assert %{"id" => id} = response = json_response(conn, 200)
279 assert response["visibility"] == "direct"
280 assert response["pleroma"]["direct_conversation_id"]
281 assert activity = Activity.get_by_id(id)
282 assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
283 assert activity.data["to"] == [user2.ap_id]
284 assert activity.data["cc"] == []
288 describe "posting scheduled statuses" do
289 setup do: oauth_access(["write:statuses"])
291 test "creates a scheduled activity", %{conn: conn} do
292 scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
295 post(conn, "/api/v1/statuses", %{
296 "status" => "scheduled",
297 "scheduled_at" => scheduled_at
300 assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200)
301 assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
302 assert [] == Repo.all(Activity)
305 test "ignores nil values", %{conn: conn} do
307 post(conn, "/api/v1/statuses", %{
308 "status" => "not scheduled",
309 "scheduled_at" => nil
312 assert result = json_response(conn, 200)
313 assert Activity.get_by_id(result["id"])
316 test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
317 scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
320 content_type: "image/jpg",
321 path: Path.absname("test/fixtures/image.jpg"),
322 filename: "an_image.jpg"
325 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
328 post(conn, "/api/v1/statuses", %{
329 "media_ids" => [to_string(upload.id)],
330 "status" => "scheduled",
331 "scheduled_at" => scheduled_at
334 assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
335 assert %{"type" => "image"} = media_attachment
338 test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
341 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
344 post(conn, "/api/v1/statuses", %{
345 "status" => "not scheduled",
346 "scheduled_at" => scheduled_at
349 assert %{"content" => "not scheduled"} = json_response(conn, 200)
350 assert [] == Repo.all(ScheduledActivity)
353 test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
355 NaiveDateTime.utc_now()
356 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
357 |> NaiveDateTime.to_iso8601()
359 attrs = %{params: %{}, scheduled_at: today}
360 {:ok, _} = ScheduledActivity.create(user, attrs)
361 {:ok, _} = ScheduledActivity.create(user, attrs)
363 conn = post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
365 assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
368 test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
370 NaiveDateTime.utc_now()
371 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
372 |> NaiveDateTime.to_iso8601()
375 NaiveDateTime.utc_now()
376 |> NaiveDateTime.add(:timer.hours(36), :millisecond)
377 |> NaiveDateTime.to_iso8601()
379 attrs = %{params: %{}, scheduled_at: today}
380 {:ok, _} = ScheduledActivity.create(user, attrs)
381 {:ok, _} = ScheduledActivity.create(user, attrs)
382 {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
385 post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
387 assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
391 describe "posting polls" do
392 setup do: oauth_access(["write:statuses"])
394 test "posting a poll", %{conn: conn} do
395 time = NaiveDateTime.utc_now()
398 post(conn, "/api/v1/statuses", %{
399 "status" => "Who is the #bestgrill?",
400 "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
403 response = json_response(conn, 200)
405 assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
406 title in ["Rei", "Asuka", "Misato"]
409 assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
410 refute response["poll"]["expred"]
412 question = Object.get_by_id(response["poll"]["id"])
414 # closed contains utc timezone
415 assert question.data["closed"] =~ "Z"
418 test "option limit is enforced", %{conn: conn} do
419 limit = Config.get([:instance, :poll_limits, :max_options])
422 post(conn, "/api/v1/statuses", %{
424 "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
427 %{"error" => error} = json_response(conn, 422)
428 assert error == "Poll can't contain more than #{limit} options"
431 test "option character limit is enforced", %{conn: conn} do
432 limit = Config.get([:instance, :poll_limits, :max_option_chars])
435 post(conn, "/api/v1/statuses", %{
438 "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
443 %{"error" => error} = json_response(conn, 422)
444 assert error == "Poll options cannot be longer than #{limit} characters each"
447 test "minimal date limit is enforced", %{conn: conn} do
448 limit = Config.get([:instance, :poll_limits, :min_expiration])
451 post(conn, "/api/v1/statuses", %{
452 "status" => "imagine arbitrary limits",
454 "options" => ["this post was made by pleroma gang"],
455 "expires_in" => limit - 1
459 %{"error" => error} = json_response(conn, 422)
460 assert error == "Expiration date is too soon"
463 test "maximum date limit is enforced", %{conn: conn} do
464 limit = Config.get([:instance, :poll_limits, :max_expiration])
467 post(conn, "/api/v1/statuses", %{
468 "status" => "imagine arbitrary limits",
470 "options" => ["this post was made by pleroma gang"],
471 "expires_in" => limit + 1
475 %{"error" => error} = json_response(conn, 422)
476 assert error == "Expiration date is too far in the future"
480 test "get a status" do
481 %{conn: conn} = oauth_access(["read:statuses"])
482 activity = insert(:note_activity)
484 conn = get(conn, "/api/v1/statuses/#{activity.id}")
486 assert %{"id" => id} = json_response(conn, 200)
487 assert id == to_string(activity.id)
490 defp local_and_remote_activities do
491 local = insert(:note_activity)
492 remote = insert(:note_activity, local: false)
493 {:ok, local: local, remote: remote}
496 describe "status with restrict unauthenticated activities for local and remote" do
497 setup do: local_and_remote_activities()
499 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
501 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
503 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
504 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
506 assert json_response(res_conn, :not_found) == %{
507 "error" => "Record not found"
510 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
512 assert json_response(res_conn, :not_found) == %{
513 "error" => "Record not found"
517 test "if user is authenticated", %{local: local, remote: remote} do
518 %{conn: conn} = oauth_access(["read"])
519 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
520 assert %{"id" => _} = json_response(res_conn, 200)
522 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
523 assert %{"id" => _} = json_response(res_conn, 200)
527 describe "status with restrict unauthenticated activities for local" do
528 setup do: local_and_remote_activities()
530 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
532 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
533 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
535 assert json_response(res_conn, :not_found) == %{
536 "error" => "Record not found"
539 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
540 assert %{"id" => _} = json_response(res_conn, 200)
543 test "if user is authenticated", %{local: local, remote: remote} do
544 %{conn: conn} = oauth_access(["read"])
545 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
546 assert %{"id" => _} = json_response(res_conn, 200)
548 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
549 assert %{"id" => _} = json_response(res_conn, 200)
553 describe "status with restrict unauthenticated activities for remote" do
554 setup do: local_and_remote_activities()
556 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
558 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
559 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
560 assert %{"id" => _} = json_response(res_conn, 200)
562 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
564 assert json_response(res_conn, :not_found) == %{
565 "error" => "Record not found"
569 test "if user is authenticated", %{local: local, remote: remote} do
570 %{conn: conn} = oauth_access(["read"])
571 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
572 assert %{"id" => _} = json_response(res_conn, 200)
574 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
575 assert %{"id" => _} = json_response(res_conn, 200)
579 test "getting a status that doesn't exist returns 404" do
580 %{conn: conn} = oauth_access(["read:statuses"])
581 activity = insert(:note_activity)
583 conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
585 assert json_response(conn, 404) == %{"error" => "Record not found"}
588 test "get a direct status" do
589 %{user: user, conn: conn} = oauth_access(["read:statuses"])
590 other_user = insert(:user)
593 CommonAPI.post(user, %{"status" => "@#{other_user.nickname}", "visibility" => "direct"})
597 |> assign(:user, user)
598 |> get("/api/v1/statuses/#{activity.id}")
600 [participation] = Participation.for_user(user)
602 res = json_response(conn, 200)
603 assert res["pleroma"]["direct_conversation_id"] == participation.id
606 test "get statuses by IDs" do
607 %{conn: conn} = oauth_access(["read:statuses"])
608 %{id: id1} = insert(:note_activity)
609 %{id: id2} = insert(:note_activity)
611 query_string = "ids[]=#{id1}&ids[]=#{id2}"
612 conn = get(conn, "/api/v1/statuses/?#{query_string}")
614 assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"])
617 describe "getting statuses by ids with restricted unauthenticated for local and remote" do
618 setup do: local_and_remote_activities()
620 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
622 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
624 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
625 res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]})
627 assert json_response(res_conn, 200) == []
630 test "if user is authenticated", %{local: local, remote: remote} do
631 %{conn: conn} = oauth_access(["read"])
633 res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]})
635 assert length(json_response(res_conn, 200)) == 2
639 describe "getting statuses by ids with restricted unauthenticated for local" do
640 setup do: local_and_remote_activities()
642 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
644 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
645 res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]})
647 remote_id = remote.id
648 assert [%{"id" => ^remote_id}] = json_response(res_conn, 200)
651 test "if user is authenticated", %{local: local, remote: remote} do
652 %{conn: conn} = oauth_access(["read"])
654 res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]})
656 assert length(json_response(res_conn, 200)) == 2
660 describe "getting statuses by ids with restricted unauthenticated for remote" do
661 setup do: local_and_remote_activities()
663 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
665 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
666 res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]})
669 assert [%{"id" => ^local_id}] = json_response(res_conn, 200)
672 test "if user is authenticated", %{local: local, remote: remote} do
673 %{conn: conn} = oauth_access(["read"])
675 res_conn = get(conn, "/api/v1/statuses", %{ids: [local.id, remote.id]})
677 assert length(json_response(res_conn, 200)) == 2
681 describe "deleting a status" do
682 test "when you created it" do
683 %{user: author, conn: conn} = oauth_access(["write:statuses"])
684 activity = insert(:note_activity, user: author)
688 |> assign(:user, author)
689 |> delete("/api/v1/statuses/#{activity.id}")
691 assert %{} = json_response(conn, 200)
693 refute Activity.get_by_id(activity.id)
696 test "when it doesn't exist" do
697 %{user: author, conn: conn} = oauth_access(["write:statuses"])
698 activity = insert(:note_activity, user: author)
702 |> assign(:user, author)
703 |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
705 assert %{"error" => "Record not found"} == json_response(conn, 404)
708 test "when you didn't create it" do
709 %{conn: conn} = oauth_access(["write:statuses"])
710 activity = insert(:note_activity)
712 conn = delete(conn, "/api/v1/statuses/#{activity.id}")
714 assert %{"error" => _} = json_response(conn, 403)
716 assert Activity.get_by_id(activity.id) == activity
719 test "when you're an admin or moderator", %{conn: conn} do
720 activity1 = insert(:note_activity)
721 activity2 = insert(:note_activity)
722 admin = insert(:user, is_admin: true)
723 moderator = insert(:user, is_moderator: true)
727 |> assign(:user, admin)
728 |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
729 |> delete("/api/v1/statuses/#{activity1.id}")
731 assert %{} = json_response(res_conn, 200)
735 |> assign(:user, moderator)
736 |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
737 |> delete("/api/v1/statuses/#{activity2.id}")
739 assert %{} = json_response(res_conn, 200)
741 refute Activity.get_by_id(activity1.id)
742 refute Activity.get_by_id(activity2.id)
746 describe "reblogging" do
747 setup do: oauth_access(["write:statuses"])
749 test "reblogs and returns the reblogged status", %{conn: conn} do
750 activity = insert(:note_activity)
752 conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog")
755 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
757 } = json_response(conn, 200)
759 assert to_string(activity.id) == id
762 test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
763 activity = insert(:note_activity)
765 conn = post(conn, "/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
767 assert %{"error" => "Record not found"} = json_response(conn, 404)
770 test "reblogs privately and returns the reblogged status", %{conn: conn} do
771 activity = insert(:note_activity)
773 conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
776 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
778 "visibility" => "private"
779 } = json_response(conn, 200)
781 assert to_string(activity.id) == id
784 test "reblogged status for another user" do
785 activity = insert(:note_activity)
786 user1 = insert(:user)
787 user2 = insert(:user)
788 user3 = insert(:user)
789 {:ok, _} = CommonAPI.favorite(user2, activity.id)
790 {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
791 {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
792 {:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
796 |> assign(:user, user3)
797 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
798 |> get("/api/v1/statuses/#{reblog_activity1.id}")
801 "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
802 "reblogged" => false,
803 "favourited" => false,
804 "bookmarked" => false
805 } = json_response(conn_res, 200)
809 |> assign(:user, user2)
810 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
811 |> get("/api/v1/statuses/#{reblog_activity1.id}")
814 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
816 "favourited" => true,
818 } = json_response(conn_res, 200)
820 assert to_string(activity.id) == id
824 describe "unreblogging" do
825 setup do: oauth_access(["write:statuses"])
827 test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
828 activity = insert(:note_activity)
830 {:ok, _, _} = CommonAPI.repeat(activity.id, user)
832 conn = post(conn, "/api/v1/statuses/#{activity.id}/unreblog")
834 assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
836 assert to_string(activity.id) == id
839 test "returns 404 error when activity does not exist", %{conn: conn} do
840 conn = post(conn, "/api/v1/statuses/foo/unreblog")
842 assert json_response(conn, 404) == %{"error" => "Record not found"}
846 describe "favoriting" do
847 setup do: oauth_access(["write:favourites"])
849 test "favs a status and returns it", %{conn: conn} do
850 activity = insert(:note_activity)
852 conn = post(conn, "/api/v1/statuses/#{activity.id}/favourite")
854 assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
855 json_response(conn, 200)
857 assert to_string(activity.id) == id
860 test "favoriting twice will just return 200", %{conn: conn} do
861 activity = insert(:note_activity)
863 post(conn, "/api/v1/statuses/#{activity.id}/favourite")
865 assert post(conn, "/api/v1/statuses/#{activity.id}/favourite")
866 |> json_response(200)
869 test "returns 404 error for a wrong id", %{conn: conn} do
872 |> post("/api/v1/statuses/1/favourite")
874 assert json_response(conn, 404) == %{"error" => "Record not found"}
878 describe "unfavoriting" do
879 setup do: oauth_access(["write:favourites"])
881 test "unfavorites a status and returns it", %{user: user, conn: conn} do
882 activity = insert(:note_activity)
884 {:ok, _} = CommonAPI.favorite(user, activity.id)
886 conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite")
888 assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
889 json_response(conn, 200)
891 assert to_string(activity.id) == id
894 test "returns 404 error for a wrong id", %{conn: conn} do
895 conn = post(conn, "/api/v1/statuses/1/unfavourite")
897 assert json_response(conn, 404) == %{"error" => "Record not found"}
901 describe "pinned statuses" do
902 setup do: oauth_access(["write:accounts"])
904 setup %{user: user} do
905 {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
907 %{activity: activity}
910 setup do: clear_config([:instance, :max_pinned_statuses], 1)
912 test "pin status", %{conn: conn, user: user, activity: activity} do
913 id_str = to_string(activity.id)
915 assert %{"id" => ^id_str, "pinned" => true} =
917 |> post("/api/v1/statuses/#{activity.id}/pin")
918 |> json_response(200)
920 assert [%{"id" => ^id_str, "pinned" => true}] =
922 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
923 |> json_response(200)
926 test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
927 {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
929 conn = post(conn, "/api/v1/statuses/#{dm.id}/pin")
931 assert json_response(conn, 400) == %{"error" => "Could not pin"}
934 test "unpin status", %{conn: conn, user: user, activity: activity} do
935 {:ok, _} = CommonAPI.pin(activity.id, user)
936 user = refresh_record(user)
938 id_str = to_string(activity.id)
940 assert %{"id" => ^id_str, "pinned" => false} =
942 |> assign(:user, user)
943 |> post("/api/v1/statuses/#{activity.id}/unpin")
944 |> json_response(200)
948 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
949 |> json_response(200)
952 test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
953 conn = post(conn, "/api/v1/statuses/1/unpin")
955 assert json_response(conn, 400) == %{"error" => "Could not unpin"}
958 test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
959 {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
961 id_str_one = to_string(activity_one.id)
963 assert %{"id" => ^id_str_one, "pinned" => true} =
965 |> post("/api/v1/statuses/#{id_str_one}/pin")
966 |> json_response(200)
968 user = refresh_record(user)
970 assert %{"error" => "You have already pinned the maximum number of statuses"} =
972 |> assign(:user, user)
973 |> post("/api/v1/statuses/#{activity_two.id}/pin")
974 |> json_response(400)
980 Config.put([:rich_media, :enabled], true)
982 oauth_access(["read:statuses"])
985 test "returns rich-media card", %{conn: conn, user: user} do
986 Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
988 {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"})
991 "image" => "http://ia.media-imdb.com/images/rock.jpg",
992 "provider_name" => "example.com",
993 "provider_url" => "https://example.com",
994 "title" => "The Rock",
996 "url" => "https://example.com/ogp",
998 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
1001 "image" => "http://ia.media-imdb.com/images/rock.jpg",
1002 "title" => "The Rock",
1003 "type" => "video.movie",
1004 "url" => "https://example.com/ogp",
1006 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
1013 |> get("/api/v1/statuses/#{activity.id}/card")
1014 |> json_response(200)
1016 assert response == card_data
1018 # works with private posts
1020 CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"})
1024 |> get("/api/v1/statuses/#{activity.id}/card")
1025 |> json_response(200)
1027 assert response_two == card_data
1030 test "replaces missing description with an empty string", %{conn: conn, user: user} do
1031 Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
1034 CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"})
1038 |> get("/api/v1/statuses/#{activity.id}/card")
1039 |> json_response(:ok)
1041 assert response == %{
1043 "title" => "Pleroma",
1044 "description" => "",
1046 "provider_name" => "example.com",
1047 "provider_url" => "https://example.com",
1048 "url" => "https://example.com/ogp-missing-data",
1051 "title" => "Pleroma",
1052 "type" => "website",
1053 "url" => "https://example.com/ogp-missing-data"
1061 bookmarks_uri = "/api/v1/bookmarks?with_relationships=true"
1063 %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
1064 author = insert(:user)
1067 CommonAPI.post(author, %{
1068 "status" => "heweoo?"
1072 CommonAPI.post(author, %{
1073 "status" => "heweoo!"
1076 response1 = post(conn, "/api/v1/statuses/#{activity1.id}/bookmark")
1078 assert json_response(response1, 200)["bookmarked"] == true
1080 response2 = post(conn, "/api/v1/statuses/#{activity2.id}/bookmark")
1082 assert json_response(response2, 200)["bookmarked"] == true
1084 bookmarks = get(conn, bookmarks_uri)
1086 assert [json_response(response2, 200), json_response(response1, 200)] ==
1087 json_response(bookmarks, 200)
1089 response1 = post(conn, "/api/v1/statuses/#{activity1.id}/unbookmark")
1091 assert json_response(response1, 200)["bookmarked"] == false
1093 bookmarks = get(conn, bookmarks_uri)
1095 assert [json_response(response2, 200)] == json_response(bookmarks, 200)
1098 describe "conversation muting" do
1099 setup do: oauth_access(["write:mutes"])
1102 post_user = insert(:user)
1103 {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"})
1104 %{activity: activity}
1107 test "mute conversation", %{conn: conn, activity: activity} do
1108 id_str = to_string(activity.id)
1110 assert %{"id" => ^id_str, "muted" => true} =
1112 |> post("/api/v1/statuses/#{activity.id}/mute")
1113 |> json_response(200)
1116 test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
1117 {:ok, _} = CommonAPI.add_mute(user, activity)
1119 conn = post(conn, "/api/v1/statuses/#{activity.id}/mute")
1121 assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
1124 test "unmute conversation", %{conn: conn, user: user, activity: activity} do
1125 {:ok, _} = CommonAPI.add_mute(user, activity)
1127 id_str = to_string(activity.id)
1129 assert %{"id" => ^id_str, "muted" => false} =
1131 # |> assign(:user, user)
1132 |> post("/api/v1/statuses/#{activity.id}/unmute")
1133 |> json_response(200)
1137 test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
1138 user1 = insert(:user)
1139 user2 = insert(:user)
1140 user3 = insert(:user)
1142 {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
1144 # Reply to status from another user
1147 |> assign(:user, user2)
1148 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
1149 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
1151 assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
1153 activity = Activity.get_by_id_with_object(id)
1155 assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
1156 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
1158 # Reblog from the third user
1161 |> assign(:user, user3)
1162 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
1163 |> post("/api/v1/statuses/#{activity.id}/reblog")
1165 assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
1166 json_response(conn2, 200)
1168 assert to_string(activity.id) == id
1170 # Getting third user status
1173 |> assign(:user, user3)
1174 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1175 |> get("api/v1/timelines/home")
1177 [reblogged_activity] = json_response(conn3, 200)
1179 assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
1181 replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
1182 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
1185 describe "GET /api/v1/statuses/:id/favourited_by" do
1186 setup do: oauth_access(["read:accounts"])
1188 setup %{user: user} do
1189 {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
1191 %{activity: activity}
1194 test "returns users who have favorited the status", %{conn: conn, activity: activity} do
1195 other_user = insert(:user)
1196 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1200 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1201 |> json_response(:ok)
1203 [%{"id" => id}] = response
1205 assert id == other_user.id
1208 test "returns empty array when status has not been favorited yet", %{
1214 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1215 |> json_response(:ok)
1217 assert Enum.empty?(response)
1220 test "does not return users who have favorited the status but are blocked", %{
1221 conn: %{assigns: %{user: user}} = conn,
1224 other_user = insert(:user)
1225 {:ok, _user_relationship} = User.block(user, other_user)
1227 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1231 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1232 |> json_response(:ok)
1234 assert Enum.empty?(response)
1237 test "does not fail on an unauthenticated request", %{activity: activity} do
1238 other_user = insert(:user)
1239 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1243 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1244 |> json_response(:ok)
1246 [%{"id" => id}] = response
1247 assert id == other_user.id
1250 test "requires authentication for private posts", %{user: user} do
1251 other_user = insert(:user)
1254 CommonAPI.post(user, %{
1255 "status" => "@#{other_user.nickname} wanna get some #cofe together?",
1256 "visibility" => "direct"
1259 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1261 favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
1264 |> get(favourited_by_url)
1265 |> json_response(404)
1269 |> assign(:user, other_user)
1270 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1273 |> assign(:token, nil)
1274 |> get(favourited_by_url)
1275 |> json_response(404)
1279 |> get(favourited_by_url)
1280 |> json_response(200)
1282 [%{"id" => id}] = response
1283 assert id == other_user.id
1287 describe "GET /api/v1/statuses/:id/reblogged_by" do
1288 setup do: oauth_access(["read:accounts"])
1290 setup %{user: user} do
1291 {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
1293 %{activity: activity}
1296 test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
1297 other_user = insert(:user)
1298 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1302 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1303 |> json_response(:ok)
1305 [%{"id" => id}] = response
1307 assert id == other_user.id
1310 test "returns empty array when status has not been reblogged yet", %{
1316 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1317 |> json_response(:ok)
1319 assert Enum.empty?(response)
1322 test "does not return users who have reblogged the status but are blocked", %{
1323 conn: %{assigns: %{user: user}} = conn,
1326 other_user = insert(:user)
1327 {:ok, _user_relationship} = User.block(user, other_user)
1329 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1333 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1334 |> json_response(:ok)
1336 assert Enum.empty?(response)
1339 test "does not return users who have reblogged the status privately", %{
1343 other_user = insert(:user)
1345 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user, %{"visibility" => "private"})
1349 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1350 |> json_response(:ok)
1352 assert Enum.empty?(response)
1355 test "does not fail on an unauthenticated request", %{activity: activity} do
1356 other_user = insert(:user)
1357 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1361 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1362 |> json_response(:ok)
1364 [%{"id" => id}] = response
1365 assert id == other_user.id
1368 test "requires authentication for private posts", %{user: user} do
1369 other_user = insert(:user)
1372 CommonAPI.post(user, %{
1373 "status" => "@#{other_user.nickname} wanna get some #cofe together?",
1374 "visibility" => "direct"
1378 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1379 |> json_response(404)
1383 |> assign(:user, other_user)
1384 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1385 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1386 |> json_response(200)
1388 assert [] == response
1393 user = insert(:user)
1395 {:ok, %{id: id1}} = CommonAPI.post(user, %{"status" => "1"})
1396 {:ok, %{id: id2}} = CommonAPI.post(user, %{"status" => "2", "in_reply_to_status_id" => id1})
1397 {:ok, %{id: id3}} = CommonAPI.post(user, %{"status" => "3", "in_reply_to_status_id" => id2})
1398 {:ok, %{id: id4}} = CommonAPI.post(user, %{"status" => "4", "in_reply_to_status_id" => id3})
1399 {:ok, %{id: id5}} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4})
1403 |> get("/api/v1/statuses/#{id3}/context")
1404 |> json_response(:ok)
1407 "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
1408 "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
1412 test "returns the favorites of a user" do
1413 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1414 other_user = insert(:user)
1416 {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
1417 {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
1419 {:ok, _} = CommonAPI.favorite(user, activity.id)
1421 first_conn = get(conn, "/api/v1/favourites")
1423 assert [status] = json_response(first_conn, 200)
1424 assert status["id"] == to_string(activity.id)
1426 assert [{"link", _link_header}] =
1427 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
1429 # Honours query params
1430 {:ok, second_activity} =
1431 CommonAPI.post(other_user, %{
1433 "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
1436 {:ok, _} = CommonAPI.favorite(user, second_activity.id)
1438 last_like = status["id"]
1440 second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
1442 assert [second_status] = json_response(second_conn, 200)
1443 assert second_status["id"] == to_string(second_activity.id)
1445 third_conn = get(conn, "/api/v1/favourites?limit=0")
1447 assert [] = json_response(third_conn, 200)
1450 test "expires_at is nil for another user" do
1451 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1452 {:ok, activity} = CommonAPI.post(user, %{"status" => "foobar", "expires_in" => 1_000_000})
1456 |> ActivityExpiration.get_by_activity_id()
1457 |> Map.get(:scheduled_at)
1458 |> NaiveDateTime.to_iso8601()
1460 assert %{"pleroma" => %{"expires_at" => ^expires_at}} =
1461 conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
1463 %{conn: conn} = oauth_access(["read:statuses"])
1465 assert %{"pleroma" => %{"expires_at" => nil}} =
1466 conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)