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.Helpers
16 alias Pleroma.Tests.ObanHelpers
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Workers.ScheduledActivityWorker
24 import Pleroma.Factory
26 setup do: clear_config([:instance, :federating])
27 setup do: clear_config([:instance, :allow_relay])
28 setup do: clear_config([:mrf, :policies])
29 setup do: clear_config([:mrf_keyword, :reject])
32 Pleroma.UnstubbedConfigMock
33 |> stub_with(Pleroma.Config)
35 Pleroma.StaticStubbedConfigMock
37 [:rich_media, :enabled] -> false
38 path -> Pleroma.Test.StaticConfig.get(path)
44 describe "posting statuses" do
45 setup do: oauth_access(["write:statuses"])
47 test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
48 clear_config([:instance, :federating], true)
49 clear_config([:instance, :allow_relay], true)
53 |> put_req_header("content-type", "application/json")
54 |> post("/api/v1/statuses", %{
55 "content_type" => "text/plain",
56 "source" => "Pleroma FE",
57 "status" => "Hello world",
58 "visibility" => "public"
60 |> json_response_and_validate_schema(200)
62 assert response["reblogs_count"] == 0
63 ObanHelpers.perform_all()
67 |> get("/api/v1/statuses/#{response["id"]}", %{})
68 |> json_response_and_validate_schema(200)
70 assert response["reblogs_count"] == 0
73 test "posting a status", %{conn: conn} do
74 idempotency_key = "Pikachu rocks!"
78 |> put_req_header("content-type", "application/json")
79 |> put_req_header("idempotency-key", idempotency_key)
80 |> post("/api/v1/statuses", %{
82 "spoiler_text" => "2hu",
86 assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
87 json_response_and_validate_schema(conn_one, 200)
89 assert Activity.get_by_id(id)
93 |> put_req_header("content-type", "application/json")
94 |> put_req_header("idempotency-key", idempotency_key)
95 |> post("/api/v1/statuses", %{
97 "spoiler_text" => "2hu",
101 # Idempotency plug response means detection fail
102 assert %{"id" => second_id} = json_response(conn_two, 200)
103 assert id == second_id
107 |> put_req_header("content-type", "application/json")
108 |> post("/api/v1/statuses", %{
110 "spoiler_text" => "2hu",
111 "sensitive" => "False"
114 assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
115 refute id == third_id
117 # An activity that will expire:
119 expires_in = 2 * 60 * 60
121 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
125 |> put_req_header("content-type", "application/json")
126 |> post("/api/v1/statuses", %{
127 "status" => "oolong",
128 "expires_in" => expires_in
131 assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
133 assert Activity.get_by_id(fourth_id)
136 worker: Pleroma.Workers.PurgeExpiredActivity,
137 args: %{activity_id: fourth_id},
138 scheduled_at: expires_at
142 test "posting a quote post", %{conn: conn} do
145 {:ok, %{id: activity_id} = activity} = CommonAPI.post(user, %{status: "yolo"})
146 %{data: %{"id" => quote_url}} = Object.normalize(activity)
150 |> put_req_header("content-type", "application/json")
151 |> post("/api/v1/statuses", %{
152 "status" => "indeed",
153 "quote_id" => activity_id
158 "pleroma" => %{"quote" => %{"id" => ^activity_id}, "quote_url" => ^quote_url}
159 } = json_response_and_validate_schema(conn, 200)
161 assert Activity.get_by_id(id)
164 test "it fails to create a status if `expires_in` is less or equal than an hour", %{
170 assert %{"error" => "Expiry date is too soon"} =
172 |> put_req_header("content-type", "application/json")
173 |> post("/api/v1/statuses", %{
174 "status" => "oolong",
175 "expires_in" => expires_in
177 |> json_response_and_validate_schema(422)
182 assert %{"error" => "Expiry date is too soon"} =
184 |> put_req_header("content-type", "application/json")
185 |> post("/api/v1/statuses", %{
186 "status" => "oolong",
187 "expires_in" => expires_in
189 |> json_response_and_validate_schema(422)
192 test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
193 clear_config([:mrf_keyword, :reject], ["GNO"])
194 clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
196 assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
198 |> put_req_header("content-type", "application/json")
199 |> post("/api/v1/statuses", %{"status" => "GNO/Linux"})
200 |> json_response_and_validate_schema(422)
203 test "posting an undefined status with an attachment", %{user: user, conn: conn} do
205 content_type: "image/jpeg",
206 path: Path.absname("test/fixtures/image.jpg"),
207 filename: "an_image.jpg"
210 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
214 |> put_req_header("content-type", "application/json")
215 |> post("/api/v1/statuses", %{
216 "media_ids" => [to_string(upload.id)]
219 assert json_response_and_validate_schema(conn, 200)
222 test "replying to a status", %{user: user, conn: conn} do
223 {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
227 |> put_req_header("content-type", "application/json")
228 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
230 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
232 activity = Activity.get_by_id(id)
234 assert activity.data["context"] == replied_to.data["context"]
235 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
238 test "replying to a direct message with visibility other than direct", %{
242 {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
244 Enum.each(["public", "private", "unlisted"], fn visibility ->
247 |> put_req_header("content-type", "application/json")
248 |> post("/api/v1/statuses", %{
249 "status" => "@#{user.nickname} hey",
250 "in_reply_to_id" => replied_to.id,
251 "visibility" => visibility
254 assert json_response_and_validate_schema(conn, 422) == %{
255 "error" => "The message visibility must be direct"
260 test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
263 |> put_req_header("content-type", "application/json")
264 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
266 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
267 assert Activity.get_by_id(id)
270 test "posting a sensitive status", %{conn: conn} do
273 |> put_req_header("content-type", "application/json")
274 |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
276 assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
277 json_response_and_validate_schema(conn, 200)
279 assert Activity.get_by_id(id)
282 test "posting a fake status", %{conn: conn} do
285 |> put_req_header("content-type", "application/json")
286 |> post("/api/v1/statuses", %{
288 "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
291 real_status = json_response_and_validate_schema(real_conn, 200)
294 assert Object.get_by_ap_id(real_status["uri"])
298 |> Map.put("id", nil)
299 |> Map.put("url", nil)
300 |> Map.put("uri", nil)
301 |> Map.put("created_at", nil)
302 |> Kernel.put_in(["pleroma", "context"], nil)
303 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
307 |> assign(:user, refresh_record(conn.assigns.user))
308 |> put_req_header("content-type", "application/json")
309 |> post("/api/v1/statuses", %{
311 "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
315 fake_status = json_response_and_validate_schema(fake_conn, 200)
318 refute Object.get_by_ap_id(fake_status["uri"])
322 |> Map.put("id", nil)
323 |> Map.put("url", nil)
324 |> Map.put("uri", nil)
325 |> Map.put("created_at", nil)
326 |> Kernel.put_in(["pleroma", "context"], nil)
327 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
329 assert real_status == fake_status
332 test "posting a direct status", %{conn: conn} do
333 user2 = insert(:user)
334 content = "direct cofe @#{user2.nickname}"
338 |> put_req_header("content-type", "application/json")
339 |> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"})
341 assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
342 assert response["visibility"] == "direct"
343 assert response["pleroma"]["direct_conversation_id"]
344 assert activity = Activity.get_by_id(id)
345 assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
346 assert activity.data["to"] == [user2.ap_id]
347 assert activity.data["cc"] == []
350 test "discloses application metadata when enabled" do
351 user = insert(:user, disclose_client: true)
352 %{user: _user, token: token, conn: conn} = oauth_access(["write:statuses"], user: user)
354 %Pleroma.Web.OAuth.Token{
355 app: %Pleroma.Web.OAuth.App{
356 client_name: app_name,
363 |> put_req_header("content-type", "application/json")
364 |> post("/api/v1/statuses", %{
365 "status" => "cofe is my copilot"
369 "content" => "cofe is my copilot"
370 } = json_response_and_validate_schema(result, 200)
372 activity = result.assigns.activity.id
376 |> get("/api/v1/statuses/#{activity}")
379 "content" => "cofe is my copilot",
382 "website" => ^app_website
384 } = json_response_and_validate_schema(result, 200)
387 test "hides application metadata when disabled" do
388 user = insert(:user, disclose_client: false)
389 %{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
393 |> put_req_header("content-type", "application/json")
394 |> post("/api/v1/statuses", %{
395 "status" => "club mate is my wingman"
398 assert %{"content" => "club mate is my wingman"} =
399 json_response_and_validate_schema(result, 200)
401 activity = result.assigns.activity.id
405 |> get("/api/v1/statuses/#{activity}")
408 "content" => "club mate is my wingman",
410 } = json_response_and_validate_schema(result, 200)
414 describe "posting scheduled statuses" do
415 setup do: oauth_access(["write:statuses"])
417 test "creates a scheduled activity", %{conn: conn} do
419 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
420 |> NaiveDateTime.to_iso8601()
425 |> put_req_header("content-type", "application/json")
426 |> post("/api/v1/statuses", %{
427 "status" => "scheduled",
428 "scheduled_at" => scheduled_at
431 assert %{"scheduled_at" => expected_scheduled_at} =
432 json_response_and_validate_schema(conn, 200)
434 assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
435 assert [] == Repo.all(Activity)
438 test "with expiration" do
439 %{conn: conn} = oauth_access(["write:statuses", "read:statuses"])
442 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
443 |> NaiveDateTime.to_iso8601()
446 assert %{"id" => status_id, "params" => %{"expires_in" => 300}} =
448 |> put_req_header("content-type", "application/json")
449 |> post("/api/v1/statuses", %{
450 "status" => "scheduled",
451 "scheduled_at" => scheduled_at,
454 |> json_response_and_validate_schema(200)
456 assert %{"id" => ^status_id, "params" => %{"expires_in" => 300}} =
458 |> put_req_header("content-type", "application/json")
459 |> get("/api/v1/scheduled_statuses/#{status_id}")
460 |> json_response_and_validate_schema(200)
463 test "ignores nil values", %{conn: conn} do
466 |> put_req_header("content-type", "application/json")
467 |> post("/api/v1/statuses", %{
468 "status" => "not scheduled",
469 "scheduled_at" => nil
472 assert result = json_response_and_validate_schema(conn, 200)
473 assert Activity.get_by_id(result["id"])
476 test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
478 NaiveDateTime.utc_now()
479 |> NaiveDateTime.add(:timer.minutes(120), :millisecond)
480 |> NaiveDateTime.to_iso8601()
484 content_type: "image/jpeg",
485 path: Path.absname("test/fixtures/image.jpg"),
486 filename: "an_image.jpg"
489 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
493 |> put_req_header("content-type", "application/json")
494 |> post("/api/v1/statuses", %{
495 "media_ids" => [to_string(upload.id)],
496 "status" => "scheduled",
497 "scheduled_at" => scheduled_at
500 assert %{"media_attachments" => [media_attachment]} =
501 json_response_and_validate_schema(conn, 200)
503 assert %{"type" => "image"} = media_attachment
506 test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
509 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
510 |> NaiveDateTime.to_iso8601()
515 |> put_req_header("content-type", "application/json")
516 |> post("/api/v1/statuses", %{
517 "status" => "not scheduled",
518 "scheduled_at" => scheduled_at
521 assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
522 assert [] == Repo.all(ScheduledActivity)
525 test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
527 NaiveDateTime.utc_now()
528 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
529 |> NaiveDateTime.to_iso8601()
533 attrs = %{params: %{}, scheduled_at: today}
534 {:ok, _} = ScheduledActivity.create(user, attrs)
535 {:ok, _} = ScheduledActivity.create(user, attrs)
539 |> put_req_header("content-type", "application/json")
540 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
542 assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
545 test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
547 NaiveDateTime.utc_now()
548 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
549 |> NaiveDateTime.to_iso8601()
553 NaiveDateTime.utc_now()
554 |> NaiveDateTime.add(:timer.hours(36), :millisecond)
555 |> NaiveDateTime.to_iso8601()
558 attrs = %{params: %{}, scheduled_at: today}
559 {:ok, _} = ScheduledActivity.create(user, attrs)
560 {:ok, _} = ScheduledActivity.create(user, attrs)
561 {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
565 |> put_req_header("content-type", "application/json")
566 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
568 assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
572 describe "posting polls" do
573 setup do: oauth_access(["write:statuses"])
575 test "posting a poll", %{conn: conn} do
576 time = NaiveDateTime.utc_now()
580 |> put_req_header("content-type", "application/json")
581 |> post("/api/v1/statuses", %{
582 "status" => "Who is the #bestgrill?",
584 "options" => ["Rei", "Asuka", "Misato"],
589 response = json_response_and_validate_schema(conn, 200)
591 assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
592 title in ["Rei", "Asuka", "Misato"]
595 assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
596 assert response["poll"]["expired"] == false
598 question = Object.get_by_id(response["poll"]["id"])
600 # closed contains utc timezone
601 assert question.data["closed"] =~ "Z"
604 test "option limit is enforced", %{conn: conn} do
605 limit = Config.get([:instance, :poll_limits, :max_options])
609 |> put_req_header("content-type", "application/json")
610 |> post("/api/v1/statuses", %{
613 "options" => Enum.map(0..limit, fn num -> "desu #{num}" end),
618 %{"error" => error} = json_response_and_validate_schema(conn, 422)
619 assert error == "Poll can't contain more than #{limit} options"
622 test "option character limit is enforced", %{conn: conn} do
623 limit = Config.get([:instance, :poll_limits, :max_option_chars])
627 |> put_req_header("content-type", "application/json")
628 |> post("/api/v1/statuses", %{
631 "options" => [String.duplicate(".", limit + 1), "lol"],
636 %{"error" => error} = json_response_and_validate_schema(conn, 422)
637 assert error == "Poll options cannot be longer than #{limit} characters each"
640 test "minimal date limit is enforced", %{conn: conn} do
641 limit = Config.get([:instance, :poll_limits, :min_expiration])
645 |> put_req_header("content-type", "application/json")
646 |> post("/api/v1/statuses", %{
647 "status" => "imagine arbitrary limits",
649 "options" => ["this post was made by pleroma gang"],
650 "expires_in" => limit - 1
654 %{"error" => error} = json_response_and_validate_schema(conn, 422)
655 assert error == "Expiration date is too soon"
658 test "maximum date limit is enforced", %{conn: conn} do
659 limit = Config.get([:instance, :poll_limits, :max_expiration])
663 |> put_req_header("content-type", "application/json")
664 |> post("/api/v1/statuses", %{
665 "status" => "imagine arbitrary limits",
667 "options" => ["this post was made by pleroma gang"],
668 "expires_in" => limit + 1
672 %{"error" => error} = json_response_and_validate_schema(conn, 422)
673 assert error == "Expiration date is too far in the future"
676 test "scheduled poll", %{conn: conn} do
677 clear_config([ScheduledActivity, :enabled], true)
680 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
681 |> NaiveDateTime.to_iso8601()
684 %{"id" => scheduled_id} =
686 |> put_req_header("content-type", "application/json")
687 |> post("/api/v1/statuses", %{
688 "status" => "very cool poll",
690 "options" => ~w(a b c),
693 "scheduled_at" => scheduled_at
695 |> json_response_and_validate_schema(200)
697 assert {:ok, %{id: activity_id}} =
698 perform_job(ScheduledActivityWorker, %{
699 activity_id: scheduled_id
702 refute_enqueued(worker: ScheduledActivityWorker)
706 |> Repo.get(activity_id)
707 |> Object.normalize()
709 assert object.data["content"] == "very cool poll"
710 assert object.data["type"] == "Question"
711 assert length(object.data["oneOf"]) == 3
714 test "cannot have only one option", %{conn: conn} do
717 |> put_req_header("content-type", "application/json")
718 |> post("/api/v1/statuses", %{
720 "poll" => %{"options" => ["mew"], "expires_in" => 1}
723 %{"error" => error} = json_response_and_validate_schema(conn, 422)
724 assert error == "Poll must contain at least 2 options"
727 test "cannot have only duplicated options", %{conn: conn} do
730 |> put_req_header("content-type", "application/json")
731 |> post("/api/v1/statuses", %{
733 "poll" => %{"options" => ["mew", "mew"], "expires_in" => 1}
736 %{"error" => error} = json_response_and_validate_schema(conn, 422)
737 assert error == "Poll must contain at least 2 options"
741 test "get a status" do
742 %{conn: conn} = oauth_access(["read:statuses"])
743 activity = insert(:note_activity)
745 conn = get(conn, "/api/v1/statuses/#{activity.id}")
747 assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
748 assert id == to_string(activity.id)
751 defp local_and_remote_activities do
752 local = insert(:note_activity)
753 remote = insert(:note_activity, local: false)
754 {:ok, local: local, remote: remote}
757 defp local_and_remote_context_activities do
758 local_user_1 = insert(:user)
759 local_user_2 = insert(:user)
760 remote_user = insert(:user, local: false)
762 {:ok, %{id: id1, data: %{"context" => context}}} =
763 CommonAPI.post(local_user_1, %{status: "post"})
765 {:ok, %{id: id2} = post} =
766 CommonAPI.post(local_user_2, %{status: "local reply", in_reply_to_status_id: id1})
769 "@context" => "https://www.w3.org/ns/activitystreams",
770 "actor" => remote_user.ap_id,
772 "context" => context,
773 "id" => "#{remote_user.ap_id}/activities/1",
774 "inReplyTo" => post.data["id"],
777 "content" => "remote reply",
778 "context" => context,
779 "id" => "#{remote_user.ap_id}/objects/1",
780 "attributedTo" => remote_user.ap_id,
784 "https://www.w3.org/ns/activitystreams#Public"
790 "https://www.w3.org/ns/activitystreams#Public"
794 {:ok, job} = Pleroma.Web.Federator.incoming_ap_doc(params)
795 {:ok, remote_activity} = ObanHelpers.perform(job)
797 %{locals: [id1, id2], remote: remote_activity.id, context: context}
800 describe "status with restrict unauthenticated activities for local and remote" do
801 setup do: local_and_remote_activities()
803 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
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}")
810 assert json_response_and_validate_schema(res_conn, :not_found) == %{
811 "error" => "Record not found"
814 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
816 assert json_response_and_validate_schema(res_conn, :not_found) == %{
817 "error" => "Record not found"
821 test "if user is authenticated", %{local: local, remote: remote} do
822 %{conn: conn} = oauth_access(["read"])
823 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
824 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
826 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
827 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
831 describe "status with restrict unauthenticated activities for local" do
832 setup do: local_and_remote_activities()
834 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
836 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
837 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
839 assert json_response_and_validate_schema(res_conn, :not_found) == %{
840 "error" => "Record not found"
843 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
844 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
847 test "if user is authenticated", %{local: local, remote: remote} do
848 %{conn: conn} = oauth_access(["read"])
849 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
850 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
852 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
853 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
857 describe "status with restrict unauthenticated activities for remote" do
858 setup do: local_and_remote_activities()
860 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
862 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
863 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
864 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
866 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
868 assert json_response_and_validate_schema(res_conn, :not_found) == %{
869 "error" => "Record not found"
873 test "if user is authenticated", %{local: local, remote: remote} do
874 %{conn: conn} = oauth_access(["read"])
875 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
876 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
878 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
879 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
883 test "getting a status that doesn't exist returns 404" do
884 %{conn: conn} = oauth_access(["read:statuses"])
885 activity = insert(:note_activity)
887 conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
889 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
892 test "get a direct status" do
893 %{user: user, conn: conn} = oauth_access(["read:statuses"])
894 other_user = insert(:user)
897 CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
901 |> assign(:user, user)
902 |> get("/api/v1/statuses/#{activity.id}")
904 [participation] = Participation.for_user(user)
906 res = json_response_and_validate_schema(conn, 200)
907 assert res["pleroma"]["direct_conversation_id"] == participation.id
910 test "get statuses by IDs" do
911 %{conn: conn} = oauth_access(["read:statuses"])
912 %{id: id1} = insert(:note_activity)
913 %{id: id2} = insert(:note_activity)
915 query_string = "ids[]=#{id1}&ids[]=#{id2}"
916 conn = get(conn, "/api/v1/statuses/?#{query_string}")
918 assert [%{"id" => ^id1}, %{"id" => ^id2}] =
919 Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
922 describe "getting statuses by ids with restricted unauthenticated for local and remote" do
923 setup do: local_and_remote_activities()
925 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
927 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
929 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
930 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
932 assert json_response_and_validate_schema(res_conn, 200) == []
935 test "if user is authenticated", %{local: local, remote: remote} do
936 %{conn: conn} = oauth_access(["read"])
938 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
940 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
944 describe "getting statuses by ids with restricted unauthenticated for local" do
945 setup do: local_and_remote_activities()
947 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
949 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
950 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
952 remote_id = remote.id
953 assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
956 test "if user is authenticated", %{local: local, remote: remote} do
957 %{conn: conn} = oauth_access(["read"])
959 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
961 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
965 describe "getting statuses by ids with restricted unauthenticated for remote" do
966 setup do: local_and_remote_activities()
968 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
970 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
971 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
974 assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
977 test "if user is authenticated", %{local: local, remote: remote} do
978 %{conn: conn} = oauth_access(["read"])
980 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
982 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
986 describe "getting status contexts restricted unauthenticated for local and remote" do
987 setup do: local_and_remote_context_activities()
989 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
991 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
993 test "if user is unauthenticated", %{conn: conn, locals: [post_id, _]} do
994 res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
996 assert json_response_and_validate_schema(res_conn, 200) == %{
1002 test "if user is unauthenticated reply", %{conn: conn, locals: [_, reply_id]} do
1003 res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
1005 assert json_response_and_validate_schema(res_conn, 200) == %{
1011 test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
1012 %{conn: conn} = oauth_access(["read"])
1013 res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
1015 %{"ancestors" => [], "descendants" => descendants} =
1016 json_response_and_validate_schema(res_conn, 200)
1020 |> Enum.map(& &1["id"])
1022 assert reply_id in descendant_ids
1023 assert remote_reply_id in descendant_ids
1026 test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
1027 %{conn: conn} = oauth_access(["read"])
1028 res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
1030 %{"ancestors" => ancestors, "descendants" => descendants} =
1031 json_response_and_validate_schema(res_conn, 200)
1035 |> Enum.map(& &1["id"])
1039 |> Enum.map(& &1["id"])
1041 assert post_id in ancestor_ids
1042 assert remote_reply_id in descendant_ids
1046 describe "getting status contexts restricted unauthenticated for local" do
1047 setup do: local_and_remote_context_activities()
1049 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
1051 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], false)
1053 test "if user is unauthenticated", %{
1055 locals: [post_id, reply_id],
1056 remote: remote_reply_id
1058 res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
1060 %{"ancestors" => [], "descendants" => descendants} =
1061 json_response_and_validate_schema(res_conn, 200)
1065 |> Enum.map(& &1["id"])
1067 assert reply_id not in descendant_ids
1068 assert remote_reply_id in descendant_ids
1071 test "if user is unauthenticated reply", %{
1073 locals: [post_id, reply_id],
1074 remote: remote_reply_id
1076 res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
1078 %{"ancestors" => ancestors, "descendants" => descendants} =
1079 json_response_and_validate_schema(res_conn, 200)
1083 |> Enum.map(& &1["id"])
1087 |> Enum.map(& &1["id"])
1089 assert post_id not in ancestor_ids
1090 assert remote_reply_id in descendant_ids
1093 test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
1094 %{conn: conn} = oauth_access(["read"])
1095 res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
1097 %{"ancestors" => [], "descendants" => descendants} =
1098 json_response_and_validate_schema(res_conn, 200)
1102 |> Enum.map(& &1["id"])
1104 assert reply_id in descendant_ids
1105 assert remote_reply_id in descendant_ids
1108 test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
1109 %{conn: conn} = oauth_access(["read"])
1110 res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
1112 %{"ancestors" => ancestors, "descendants" => descendants} =
1113 json_response_and_validate_schema(res_conn, 200)
1117 |> Enum.map(& &1["id"])
1121 |> Enum.map(& &1["id"])
1123 assert post_id in ancestor_ids
1124 assert remote_reply_id in descendant_ids
1128 describe "getting status contexts restricted unauthenticated for remote" do
1129 setup do: local_and_remote_context_activities()
1131 setup do: clear_config([:restrict_unauthenticated, :activities, :local], false)
1133 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
1135 test "if user is unauthenticated", %{
1137 locals: [post_id, reply_id],
1138 remote: remote_reply_id
1140 res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
1142 %{"ancestors" => [], "descendants" => descendants} =
1143 json_response_and_validate_schema(res_conn, 200)
1147 |> Enum.map(& &1["id"])
1149 assert reply_id in descendant_ids
1150 assert remote_reply_id not in descendant_ids
1153 test "if user is unauthenticated reply", %{
1155 locals: [post_id, reply_id],
1156 remote: remote_reply_id
1158 res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
1160 %{"ancestors" => ancestors, "descendants" => descendants} =
1161 json_response_and_validate_schema(res_conn, 200)
1165 |> Enum.map(& &1["id"])
1169 |> Enum.map(& &1["id"])
1171 assert post_id in ancestor_ids
1172 assert remote_reply_id not in descendant_ids
1175 test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
1176 %{conn: conn} = oauth_access(["read"])
1177 res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
1179 %{"ancestors" => [], "descendants" => descendants} =
1180 json_response_and_validate_schema(res_conn, 200)
1184 |> Enum.map(& &1["id"])
1186 assert reply_id in reply_ids
1187 assert remote_reply_id in reply_ids
1190 test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
1191 %{conn: conn} = oauth_access(["read"])
1192 res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
1194 %{"ancestors" => ancestors, "descendants" => descendants} =
1195 json_response_and_validate_schema(res_conn, 200)
1199 |> Enum.map(& &1["id"])
1203 |> Enum.map(& &1["id"])
1205 assert post_id in ancestor_ids
1206 assert remote_reply_id in descendant_ids
1210 describe "deleting a status" do
1211 test "when you created it" do
1212 %{user: author, conn: conn} = oauth_access(["write:statuses"])
1213 activity = insert(:note_activity, user: author)
1214 object = Object.normalize(activity, fetch: false)
1216 content = object.data["content"]
1217 source = object.data["source"]
1221 |> assign(:user, author)
1222 |> delete("/api/v1/statuses/#{activity.id}")
1223 |> json_response_and_validate_schema(200)
1225 assert match?(%{"content" => ^content, "text" => ^source}, result)
1227 refute Activity.get_by_id(activity.id)
1230 test "when it doesn't exist" do
1231 %{user: author, conn: conn} = oauth_access(["write:statuses"])
1232 activity = insert(:note_activity, user: author)
1236 |> assign(:user, author)
1237 |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
1239 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
1242 test "when you didn't create it" do
1243 %{conn: conn} = oauth_access(["write:statuses"])
1244 activity = insert(:note_activity)
1246 conn = delete(conn, "/api/v1/statuses/#{activity.id}")
1248 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
1250 assert Activity.get_by_id(activity.id) == activity
1253 test "when you're privileged to", %{conn: conn} do
1254 clear_config([:instance, :moderator_privileges], [:messages_delete])
1255 activity = insert(:note_activity)
1256 user = insert(:user, is_moderator: true)
1260 |> assign(:user, user)
1261 |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
1262 |> delete("/api/v1/statuses/#{activity.id}")
1264 assert %{} = json_response_and_validate_schema(res_conn, 200)
1266 assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() ==
1267 "@#{user.nickname} deleted status ##{activity.id}"
1269 refute Activity.get_by_id(activity.id)
1272 test "when you're privileged and the user is banned", %{conn: conn} do
1273 clear_config([:instance, :moderator_privileges], [:messages_delete])
1274 posting_user = insert(:user, is_active: false)
1275 refute posting_user.is_active
1276 activity = insert(:note_activity, user: posting_user)
1277 user = insert(:user, is_moderator: true)
1281 |> assign(:user, user)
1282 |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
1283 |> delete("/api/v1/statuses/#{activity.id}")
1285 assert %{} = json_response_and_validate_schema(res_conn, 200)
1287 assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() ==
1288 "@#{user.nickname} deleted status ##{activity.id}"
1290 refute Activity.get_by_id(activity.id)
1294 describe "reblogging" do
1295 setup do: oauth_access(["write:statuses"])
1297 test "reblogs and returns the reblogged status", %{conn: conn} do
1298 activity = insert(:note_activity)
1302 |> put_req_header("content-type", "application/json")
1303 |> post("/api/v1/statuses/#{activity.id}/reblog")
1306 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1308 } = json_response_and_validate_schema(conn, 200)
1310 assert to_string(activity.id) == id
1313 test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
1314 activity = insert(:note_activity)
1318 |> put_req_header("content-type", "application/json")
1319 |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
1321 assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
1324 test "reblogs privately and returns the reblogged status", %{conn: conn} do
1325 activity = insert(:note_activity)
1329 |> put_req_header("content-type", "application/json")
1331 "/api/v1/statuses/#{activity.id}/reblog",
1332 %{"visibility" => "private"}
1336 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1337 "reblogged" => true,
1338 "visibility" => "private"
1339 } = json_response_and_validate_schema(conn, 200)
1341 assert to_string(activity.id) == id
1344 test "reblogged status for another user" do
1345 activity = insert(:note_activity)
1346 user1 = insert(:user)
1347 user2 = insert(:user)
1348 user3 = insert(:user)
1349 {:ok, _} = CommonAPI.favorite(user2, activity.id)
1350 {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
1351 {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
1352 {:ok, _} = CommonAPI.repeat(activity.id, user2)
1356 |> assign(:user, user3)
1357 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1358 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1361 "reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
1362 "reblogged" => false,
1363 "favourited" => false,
1364 "bookmarked" => false
1365 } = json_response_and_validate_schema(conn_res, 200)
1369 |> assign(:user, user2)
1370 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
1371 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1374 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
1375 "reblogged" => true,
1376 "favourited" => true,
1377 "bookmarked" => true
1378 } = json_response_and_validate_schema(conn_res, 200)
1380 assert to_string(activity.id) == id
1383 test "author can reblog own private status", %{conn: conn, user: user} do
1384 {:ok, activity} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
1388 |> put_req_header("content-type", "application/json")
1389 |> post("/api/v1/statuses/#{activity.id}/reblog")
1392 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1393 "reblogged" => true,
1394 "visibility" => "private"
1395 } = json_response_and_validate_schema(conn, 200)
1397 assert to_string(activity.id) == id
1401 describe "unreblogging" do
1402 setup do: oauth_access(["write:statuses"])
1404 test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
1405 activity = insert(:note_activity)
1407 {:ok, _} = CommonAPI.repeat(activity.id, user)
1411 |> put_req_header("content-type", "application/json")
1412 |> post("/api/v1/statuses/#{activity.id}/unreblog")
1414 assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
1415 json_response_and_validate_schema(conn, 200)
1417 assert to_string(activity.id) == id
1420 test "returns 404 error when activity does not exist", %{conn: conn} do
1423 |> put_req_header("content-type", "application/json")
1424 |> post("/api/v1/statuses/foo/unreblog")
1426 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1430 describe "favoriting" do
1431 setup do: oauth_access(["write:favourites"])
1433 test "favs a status and returns it", %{conn: conn} do
1434 activity = insert(:note_activity)
1438 |> put_req_header("content-type", "application/json")
1439 |> post("/api/v1/statuses/#{activity.id}/favourite")
1441 assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
1442 json_response_and_validate_schema(conn, 200)
1444 assert to_string(activity.id) == id
1447 test "favoriting twice will just return 200", %{conn: conn} do
1448 activity = insert(:note_activity)
1451 |> put_req_header("content-type", "application/json")
1452 |> post("/api/v1/statuses/#{activity.id}/favourite")
1455 |> put_req_header("content-type", "application/json")
1456 |> post("/api/v1/statuses/#{activity.id}/favourite")
1457 |> json_response_and_validate_schema(200)
1460 test "returns 404 error for a wrong id", %{conn: conn} do
1463 |> put_req_header("content-type", "application/json")
1464 |> post("/api/v1/statuses/1/favourite")
1466 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1470 describe "unfavoriting" do
1471 setup do: oauth_access(["write:favourites"])
1473 test "unfavorites a status and returns it", %{user: user, conn: conn} do
1474 activity = insert(:note_activity)
1476 {:ok, _} = CommonAPI.favorite(user, activity.id)
1480 |> put_req_header("content-type", "application/json")
1481 |> post("/api/v1/statuses/#{activity.id}/unfavourite")
1483 assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
1484 json_response_and_validate_schema(conn, 200)
1486 assert to_string(activity.id) == id
1489 test "returns 404 error for a wrong id", %{conn: conn} do
1492 |> put_req_header("content-type", "application/json")
1493 |> post("/api/v1/statuses/1/unfavourite")
1495 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1499 describe "pinned statuses" do
1500 setup do: oauth_access(["write:accounts"])
1502 setup %{user: user} do
1503 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
1505 %{activity: activity}
1508 setup do: clear_config([:instance, :max_pinned_statuses], 1)
1510 test "pin status", %{conn: conn, user: user, activity: activity} do
1513 assert %{"id" => ^id, "pinned" => true} =
1515 |> put_req_header("content-type", "application/json")
1516 |> post("/api/v1/statuses/#{activity.id}/pin")
1517 |> json_response_and_validate_schema(200)
1519 assert [%{"id" => ^id, "pinned" => true}] =
1521 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1522 |> json_response_and_validate_schema(200)
1525 test "non authenticated user", %{activity: activity} do
1527 |> put_req_header("content-type", "application/json")
1528 |> post("/api/v1/statuses/#{activity.id}/pin")
1529 |> json_response(403) == %{"error" => "Invalid credentials."}
1532 test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
1533 {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
1537 |> put_req_header("content-type", "application/json")
1538 |> post("/api/v1/statuses/#{dm.id}/pin")
1540 assert json_response_and_validate_schema(conn, 422) == %{
1541 "error" => "Non-public status cannot be pinned"
1545 test "pin by another user", %{activity: activity} do
1546 %{conn: conn} = oauth_access(["write:accounts"])
1549 |> put_req_header("content-type", "application/json")
1550 |> post("/api/v1/statuses/#{activity.id}/pin")
1551 |> json_response(422) == %{"error" => "Someone else's status cannot be pinned"}
1554 test "unpin status", %{conn: conn, user: user, activity: activity} do
1555 {:ok, _} = CommonAPI.pin(activity.id, user)
1556 user = refresh_record(user)
1558 id_str = to_string(activity.id)
1560 assert %{"id" => ^id_str, "pinned" => false} =
1562 |> assign(:user, user)
1563 |> post("/api/v1/statuses/#{activity.id}/unpin")
1564 |> json_response_and_validate_schema(200)
1568 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1569 |> json_response_and_validate_schema(200)
1572 test "/unpin: returns 404 error when activity doesn't exist", %{conn: conn} do
1574 |> put_req_header("content-type", "application/json")
1575 |> post("/api/v1/statuses/1/unpin")
1576 |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
1579 test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
1580 {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
1582 id_str_one = to_string(activity_one.id)
1584 assert %{"id" => ^id_str_one, "pinned" => true} =
1586 |> put_req_header("content-type", "application/json")
1587 |> post("/api/v1/statuses/#{id_str_one}/pin")
1588 |> json_response_and_validate_schema(200)
1590 user = refresh_record(user)
1592 assert %{"error" => "You have already pinned the maximum number of statuses"} =
1594 |> assign(:user, user)
1595 |> post("/api/v1/statuses/#{activity_two.id}/pin")
1596 |> json_response_and_validate_schema(400)
1599 test "on pin removes deletion job, on unpin reschedule deletion" do
1600 %{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
1601 expires_in = 2 * 60 * 60
1603 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
1605 assert %{"id" => id} =
1607 |> put_req_header("content-type", "application/json")
1608 |> post("/api/v1/statuses", %{
1609 "status" => "oolong",
1610 "expires_in" => expires_in
1612 |> json_response_and_validate_schema(200)
1615 worker: Pleroma.Workers.PurgeExpiredActivity,
1616 args: %{activity_id: id},
1617 scheduled_at: expires_at
1620 assert %{"id" => ^id, "pinned" => true} =
1622 |> put_req_header("content-type", "application/json")
1623 |> post("/api/v1/statuses/#{id}/pin")
1624 |> json_response_and_validate_schema(200)
1627 worker: Pleroma.Workers.PurgeExpiredActivity,
1628 args: %{activity_id: id},
1629 scheduled_at: expires_at
1632 assert %{"id" => ^id, "pinned" => false} =
1634 |> put_req_header("content-type", "application/json")
1635 |> post("/api/v1/statuses/#{id}/unpin")
1636 |> json_response_and_validate_schema(200)
1639 worker: Pleroma.Workers.PurgeExpiredActivity,
1640 args: %{activity_id: id},
1641 scheduled_at: expires_at
1647 bookmarks_uri = "/api/v1/bookmarks"
1649 %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
1650 author = insert(:user)
1652 {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
1653 {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
1657 |> put_req_header("content-type", "application/json")
1658 |> post("/api/v1/statuses/#{activity1.id}/bookmark")
1660 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
1664 |> put_req_header("content-type", "application/json")
1665 |> post("/api/v1/statuses/#{activity2.id}/bookmark")
1667 assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
1669 bookmarks = get(conn, bookmarks_uri)
1672 json_response_and_validate_schema(response2, 200),
1673 json_response_and_validate_schema(response1, 200)
1675 json_response_and_validate_schema(bookmarks, 200)
1679 |> put_req_header("content-type", "application/json")
1680 |> post("/api/v1/statuses/#{activity1.id}/unbookmark")
1682 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
1684 bookmarks = get(conn, bookmarks_uri)
1686 assert [json_response_and_validate_schema(response2, 200)] ==
1687 json_response_and_validate_schema(bookmarks, 200)
1690 test "bookmark folders" do
1691 %{conn: conn, user: user} = oauth_access(["write:bookmarks", "read:bookmarks"])
1693 {:ok, folder} = Pleroma.BookmarkFolder.create(user.id, "folder")
1694 author = insert(:user)
1696 folder_bookmarks_uri = "/api/v1/bookmarks?folder_id=#{folder.id}"
1698 {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
1699 {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
1701 # Add bookmark with a folder
1704 |> put_req_header("content-type", "application/json")
1705 |> post("/api/v1/statuses/#{activity1.id}/bookmark", %{folder_id: folder.id})
1707 assert json_response_and_validate_schema(response, 200)["bookmarked"] == true
1709 assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] ==
1714 |> put_req_header("content-type", "application/json")
1715 |> post("/api/v1/statuses/#{activity2.id}/bookmark")
1717 assert json_response_and_validate_schema(response, 200)["bookmarked"] == true
1718 assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] == nil
1721 get(conn, folder_bookmarks_uri)
1722 |> json_response_and_validate_schema(200)
1724 assert length(bookmarks) == 1
1726 # Update folder for existing bookmark
1729 |> put_req_header("content-type", "application/json")
1730 |> post("/api/v1/statuses/#{activity2.id}/bookmark", %{folder_id: folder.id})
1732 assert json_response_and_validate_schema(response, 200)["bookmarked"] == true
1734 assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] ==
1738 get(conn, folder_bookmarks_uri)
1739 |> json_response_and_validate_schema(200)
1741 assert length(bookmarks) == 2
1744 describe "conversation muting" do
1745 setup do: oauth_access(["write:mutes"])
1748 post_user = insert(:user)
1749 {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
1750 %{activity: activity}
1753 test "mute conversation", %{conn: conn, activity: activity} do
1754 id_str = to_string(activity.id)
1756 assert %{"id" => ^id_str, "muted" => true} =
1758 |> put_req_header("content-type", "application/json")
1759 |> post("/api/v1/statuses/#{activity.id}/mute")
1760 |> json_response_and_validate_schema(200)
1763 test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
1764 {:ok, _} = CommonAPI.add_mute(user, activity)
1768 |> put_req_header("content-type", "application/json")
1769 |> post("/api/v1/statuses/#{activity.id}/mute")
1771 assert json_response_and_validate_schema(conn, 400) == %{
1772 "error" => "conversation is already muted"
1776 test "unmute conversation", %{conn: conn, user: user, activity: activity} do
1777 {:ok, _} = CommonAPI.add_mute(user, activity)
1779 id_str = to_string(activity.id)
1781 assert %{"id" => ^id_str, "muted" => false} =
1783 # |> assign(:user, user)
1784 |> post("/api/v1/statuses/#{activity.id}/unmute")
1785 |> json_response_and_validate_schema(200)
1789 test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
1790 user1 = insert(:user)
1791 user2 = insert(:user)
1792 user3 = insert(:user)
1794 {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
1796 # Reply to status from another user
1799 |> assign(:user, user2)
1800 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
1801 |> put_req_header("content-type", "application/json")
1802 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
1804 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
1806 activity = Activity.get_by_id_with_object(id)
1808 assert Object.normalize(activity, fetch: false).data["inReplyTo"] ==
1809 Object.normalize(replied_to, fetch: false).data["id"]
1811 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
1813 # Reblog from the third user
1816 |> assign(:user, user3)
1817 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
1818 |> put_req_header("content-type", "application/json")
1819 |> post("/api/v1/statuses/#{activity.id}/reblog")
1821 assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
1822 json_response_and_validate_schema(conn2, 200)
1824 assert to_string(activity.id) == id
1826 # Getting third user status
1829 |> assign(:user, user3)
1830 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1831 |> get("/api/v1/timelines/home")
1833 [reblogged_activity] = json_response_and_validate_schema(conn3, 200)
1835 assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
1837 replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
1838 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
1841 describe "GET /api/v1/statuses/:id/favourited_by" do
1842 setup do: oauth_access(["read:accounts"])
1844 setup %{user: user} do
1845 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1847 %{activity: activity}
1850 test "returns users who have favorited the status", %{conn: conn, activity: activity} do
1851 other_user = insert(:user)
1852 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1856 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1857 |> json_response_and_validate_schema(:ok)
1859 [%{"id" => id}] = response
1861 assert id == other_user.id
1864 test "returns empty array when status has not been favorited yet", %{
1870 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1871 |> json_response_and_validate_schema(:ok)
1873 assert Enum.empty?(response)
1876 test "does not return users who have favorited the status but are blocked", %{
1877 conn: %{assigns: %{user: user}} = conn,
1880 other_user = insert(:user)
1881 {:ok, _user_relationship} = User.block(user, other_user)
1883 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1887 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1888 |> json_response_and_validate_schema(:ok)
1890 assert Enum.empty?(response)
1893 test "does not fail on an unauthenticated request", %{activity: activity} do
1894 other_user = insert(:user)
1895 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1899 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1900 |> json_response_and_validate_schema(:ok)
1902 [%{"id" => id}] = response
1903 assert id == other_user.id
1906 test "requires authentication for private posts", %{user: user} do
1907 other_user = insert(:user)
1910 CommonAPI.post(user, %{
1911 status: "@#{other_user.nickname} wanna get some #cofe together?",
1912 visibility: "direct"
1915 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1917 favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
1920 |> get(favourited_by_url)
1921 |> json_response_and_validate_schema(404)
1925 |> assign(:user, other_user)
1926 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1929 |> assign(:token, nil)
1930 |> get(favourited_by_url)
1931 |> json_response_and_validate_schema(404)
1935 |> get(favourited_by_url)
1936 |> json_response_and_validate_schema(200)
1938 [%{"id" => id}] = response
1939 assert id == other_user.id
1942 test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
1943 clear_config([:instance, :show_reactions], false)
1945 other_user = insert(:user)
1946 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1950 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1951 |> json_response_and_validate_schema(:ok)
1953 assert Enum.empty?(response)
1957 describe "GET /api/v1/statuses/:id/reblogged_by" do
1958 setup do: oauth_access(["read:accounts"])
1960 setup %{user: user} do
1961 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1963 %{activity: activity}
1966 test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
1967 other_user = insert(:user)
1968 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1972 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1973 |> json_response_and_validate_schema(:ok)
1975 [%{"id" => id}] = response
1977 assert id == other_user.id
1980 test "returns empty array when status has not been reblogged yet", %{
1986 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1987 |> json_response_and_validate_schema(:ok)
1989 assert Enum.empty?(response)
1992 test "does not return users who have reblogged the status but are blocked", %{
1993 conn: %{assigns: %{user: user}} = conn,
1996 other_user = insert(:user)
1997 {:ok, _user_relationship} = User.block(user, other_user)
1999 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
2003 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2004 |> json_response_and_validate_schema(:ok)
2006 assert Enum.empty?(response)
2009 test "does not return users who have reblogged the status privately", %{
2012 other_user = insert(:user)
2013 {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
2015 {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
2019 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2020 |> json_response_and_validate_schema(:ok)
2022 assert Enum.empty?(response)
2025 test "does not fail on an unauthenticated request", %{activity: activity} do
2026 other_user = insert(:user)
2027 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
2031 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2032 |> json_response_and_validate_schema(:ok)
2034 [%{"id" => id}] = response
2035 assert id == other_user.id
2038 test "requires authentication for private posts", %{user: user} do
2039 other_user = insert(:user)
2042 CommonAPI.post(user, %{
2043 status: "@#{other_user.nickname} wanna get some #cofe together?",
2044 visibility: "direct"
2048 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2049 |> json_response_and_validate_schema(404)
2053 |> assign(:user, other_user)
2054 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
2055 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
2056 |> json_response_and_validate_schema(200)
2058 assert [] == response
2063 user = insert(:user)
2065 {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
2066 {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
2067 {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
2068 {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
2069 {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
2073 |> get("/api/v1/statuses/#{id3}/context")
2074 |> json_response_and_validate_schema(:ok)
2077 "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
2078 "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
2082 test "favorites paginate correctly" do
2083 %{user: user, conn: conn} = oauth_access(["read:favourites"])
2084 other_user = insert(:user)
2085 {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
2086 {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
2087 {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
2089 {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
2090 {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
2091 {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
2095 |> get("/api/v1/favourites?limit=1")
2097 assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
2098 assert post_id == second_post.id
2100 # Using the header for pagination works correctly
2101 [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
2102 [next_url, _next_rel] = String.split(next, ";")
2103 next_url = String.trim_trailing(next_url, ">") |> String.trim_leading("<")
2105 max_id = Helpers.get_query_parameter(next_url, "max_id")
2107 assert max_id == third_favorite.id
2111 |> get("/api/v1/favourites?max_id=#{max_id}")
2113 assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
2114 json_response_and_validate_schema(result, 200)
2116 assert first_post_id == first_post.id
2117 assert third_post_id == third_post.id
2120 test "returns the favorites of a user" do
2121 %{user: user, conn: conn} = oauth_access(["read:favourites"])
2122 other_user = insert(:user)
2124 {:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
2125 {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
2127 {:ok, last_like} = CommonAPI.favorite(user, activity.id)
2129 first_conn = get(conn, "/api/v1/favourites")
2131 assert [status] = json_response_and_validate_schema(first_conn, 200)
2132 assert status["id"] == to_string(activity.id)
2134 assert [{"link", _link_header}] =
2135 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
2137 # Honours query params
2138 {:ok, second_activity} =
2139 CommonAPI.post(other_user, %{
2140 status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
2143 {:ok, _} = CommonAPI.favorite(user, second_activity.id)
2145 second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
2147 assert [second_status] = json_response_and_validate_schema(second_conn, 200)
2148 assert second_status["id"] == to_string(second_activity.id)
2150 third_conn = get(conn, "/api/v1/favourites?limit=0")
2152 assert [] = json_response_and_validate_schema(third_conn, 200)
2155 test "expires_at is nil for another user" do
2156 %{conn: conn, user: user} = oauth_access(["read:statuses"])
2157 expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
2158 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
2160 assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
2162 |> get("/api/v1/statuses/#{activity.id}")
2163 |> json_response_and_validate_schema(:ok)
2165 {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
2166 assert DateTime.diff(expires_at, a_expires_at) == 0
2168 %{conn: conn} = oauth_access(["read:statuses"])
2170 assert %{"pleroma" => %{"expires_at" => nil}} =
2172 |> get("/api/v1/statuses/#{activity.id}")
2173 |> json_response_and_validate_schema(:ok)
2176 describe "local-only statuses" do
2177 test "posting a local only status" do
2178 %{user: _user, conn: conn} = oauth_access(["write:statuses"])
2182 |> put_req_header("content-type", "application/json")
2183 |> post("/api/v1/statuses", %{
2185 "visibility" => "local"
2188 local = Utils.as_local_public()
2190 assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
2191 json_response_and_validate_schema(conn_one, 200)
2193 assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
2196 test "other users can read local-only posts" do
2197 user = insert(:user)
2198 %{user: _reader, conn: conn} = oauth_access(["read:statuses"])
2200 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
2204 |> get("/api/v1/statuses/#{activity.id}")
2205 |> json_response_and_validate_schema(:ok)
2207 assert received["id"] == activity.id
2210 test "anonymous users cannot see local-only posts" do
2211 user = insert(:user)
2213 {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
2217 |> get("/api/v1/statuses/#{activity.id}")
2218 |> json_response_and_validate_schema(:not_found)
2222 describe "muted reactions" do
2224 %{conn: conn, user: user} = oauth_access(["read:statuses"])
2226 other_user = insert(:user)
2227 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
2229 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
2230 User.mute(user, other_user)
2234 |> get("/api/v1/statuses/?ids[]=#{activity.id}")
2235 |> json_response_and_validate_schema(200)
2240 "emoji_reactions" => []
2247 |> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
2248 |> json_response_and_validate_schema(200)
2253 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
2260 # %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
2261 %{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
2263 other_user = insert(:user)
2264 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
2266 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
2267 User.mute(user, other_user)
2271 |> get("/api/v1/statuses/#{activity.id}")
2272 |> json_response_and_validate_schema(200)
2276 "emoji_reactions" => []
2282 |> get("/api/v1/statuses/#{activity.id}?with_muted=true")
2283 |> json_response_and_validate_schema(200)
2287 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
2293 describe "get status history" do
2295 %{conn: build_conn()}
2298 test "unedited post", %{conn: conn} do
2299 activity = insert(:note_activity)
2301 conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
2303 assert [_] = json_response_and_validate_schema(conn, 200)
2306 test "edited post", %{conn: conn} do
2311 "formerRepresentations" => %{
2312 "type" => "OrderedCollection",
2316 "content" => "mew mew 2",
2317 "summary" => "title 2"
2321 "content" => "mew mew 1",
2322 "summary" => "title 1"
2330 activity = insert(:note_activity, note: note)
2332 conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
2334 assert [%{"spoiler_text" => "title 1"}, %{"spoiler_text" => "title 2"}, _] =
2335 json_response_and_validate_schema(conn, 200)
2339 describe "get status source" do
2341 %{conn: build_conn()}
2344 test "it returns the source", %{conn: conn} do
2345 user = insert(:user)
2347 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2349 conn = get(conn, "/api/v1/statuses/#{activity.id}/source")
2353 assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
2354 json_response_and_validate_schema(conn, 200)
2358 describe "update status" do
2360 oauth_access(["write:statuses"])
2363 test "it updates the status" do
2364 %{conn: conn, user: user} = oauth_access(["write:statuses", "read:statuses"])
2366 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2369 |> get("/api/v1/statuses/#{activity.id}")
2370 |> json_response_and_validate_schema(200)
2374 |> put_req_header("content-type", "application/json")
2375 |> put("/api/v1/statuses/#{activity.id}", %{
2376 "status" => "edited",
2377 "spoiler_text" => "lol"
2379 |> json_response_and_validate_schema(200)
2381 assert response["content"] == "edited"
2382 assert response["spoiler_text"] == "lol"
2386 |> get("/api/v1/statuses/#{activity.id}")
2387 |> json_response_and_validate_schema(200)
2389 assert response["content"] == "edited"
2390 assert response["spoiler_text"] == "lol"
2393 test "it updates the attachments", %{conn: conn, user: user} do
2394 attachment = insert(:attachment, user: user)
2395 attachment_id = to_string(attachment.id)
2397 {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
2401 |> put_req_header("content-type", "application/json")
2402 |> put("/api/v1/statuses/#{activity.id}", %{
2403 "status" => "mew mew #abc",
2404 "spoiler_text" => "#def",
2405 "media_ids" => [attachment_id]
2407 |> json_response_and_validate_schema(200)
2409 assert [%{"id" => ^attachment_id}] = response["media_attachments"]
2412 test "it does not update visibility", %{conn: conn, user: user} do
2414 CommonAPI.post(user, %{
2415 status: "mew mew #abc",
2416 spoiler_text: "#def",
2417 visibility: "private"
2422 |> put_req_header("content-type", "application/json")
2423 |> put("/api/v1/statuses/#{activity.id}", %{
2424 "status" => "edited",
2425 "spoiler_text" => "lol"
2427 |> json_response_and_validate_schema(200)
2429 assert response["visibility"] == "private"
2432 test "it refuses to update when original post is not by the user", %{conn: conn} do
2433 another_user = insert(:user)
2436 CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
2439 |> put_req_header("content-type", "application/json")
2440 |> put("/api/v1/statuses/#{activity.id}", %{
2441 "status" => "edited",
2442 "spoiler_text" => "lol"
2444 |> json_response_and_validate_schema(:forbidden)
2447 test "it returns 404 if the user cannot see the post", %{conn: conn} do
2448 another_user = insert(:user)
2451 CommonAPI.post(another_user, %{
2452 status: "mew mew #abc",
2453 spoiler_text: "#def",
2454 visibility: "private"
2458 |> put_req_header("content-type", "application/json")
2459 |> put("/api/v1/statuses/#{activity.id}", %{
2460 "status" => "edited",
2461 "spoiler_text" => "lol"
2463 |> json_response_and_validate_schema(:not_found)