1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
6 use Pleroma.Web.ConnCase
10 alias Pleroma.Notification
13 alias Pleroma.Tests.ObanHelpers
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.OAuth.App
18 alias Pleroma.Web.Push
20 import ExUnit.CaptureLog
21 import Pleroma.Factory
22 import Swoosh.TestAssertions
26 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
30 clear_config([:instance, :public])
31 clear_config([:rich_media, :enabled])
33 test "apps/verify_credentials", %{conn: conn} do
34 token = insert(:oauth_token)
38 |> assign(:user, token.user)
39 |> assign(:token, token)
40 |> get("/api/v1/apps/verify_credentials")
42 app = Repo.preload(token, :app).app
45 "name" => app.client_name,
46 "website" => app.website,
47 "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
50 assert expected == json_response(conn, 200)
53 test "creates an oauth app", %{conn: conn} do
55 app_attrs = build(:oauth_app)
59 |> assign(:user, user)
60 |> post("/api/v1/apps", %{
61 client_name: app_attrs.client_name,
62 redirect_uris: app_attrs.redirect_uris
68 "name" => app.client_name,
69 "website" => app.website,
70 "client_id" => app.client_id,
71 "client_secret" => app.client_secret,
72 "id" => app.id |> to_string(),
73 "redirect_uri" => app.redirect_uris,
74 "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
77 assert expected == json_response(conn, 200)
80 describe "media upload" do
86 |> assign(:user, user)
89 content_type: "image/jpg",
90 path: Path.absname("test/fixtures/image.jpg"),
91 filename: "an_image.jpg"
94 [conn: conn, image: image]
97 clear_config([:media_proxy])
98 clear_config([Pleroma.Upload])
100 test "returns uploaded image", %{conn: conn, image: image} do
101 desc = "Description of the image"
105 |> post("/api/v1/media", %{"file" => image, "description" => desc})
106 |> json_response(:ok)
108 assert media["type"] == "image"
109 assert media["description"] == desc
112 object = Repo.get(Object, media["id"])
113 assert object.data["actor"] == User.ap_id(conn.assigns[:user])
117 describe "/api/v1/pleroma/mascot" do
118 test "mascot upload", %{conn: conn} do
121 non_image_file = %Plug.Upload{
122 content_type: "audio/mpeg",
123 path: Path.absname("test/fixtures/sound.mp3"),
124 filename: "sound.mp3"
129 |> assign(:user, user)
130 |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file})
132 assert json_response(conn, 415)
135 content_type: "image/jpg",
136 path: Path.absname("test/fixtures/image.jpg"),
137 filename: "an_image.jpg"
142 |> assign(:user, user)
143 |> put("/api/v1/pleroma/mascot", %{"file" => file})
145 assert %{"id" => _, "type" => image} = json_response(conn, 200)
148 test "mascot retrieving", %{conn: conn} do
150 # When user hasn't set a mascot, we should just get pleroma tan back
153 |> assign(:user, user)
154 |> get("/api/v1/pleroma/mascot")
156 assert %{"url" => url} = json_response(conn, 200)
157 assert url =~ "pleroma-fox-tan-smol"
159 # When a user sets their mascot, we should get that back
161 content_type: "image/jpg",
162 path: Path.absname("test/fixtures/image.jpg"),
163 filename: "an_image.jpg"
168 |> assign(:user, user)
169 |> put("/api/v1/pleroma/mascot", %{"file" => file})
171 assert json_response(conn, 200)
173 user = User.get_cached_by_id(user.id)
177 |> assign(:user, user)
178 |> get("/api/v1/pleroma/mascot")
180 assert %{"url" => url, "type" => "image"} = json_response(conn, 200)
181 assert url =~ "an_image"
185 test "getting a list of mutes", %{conn: conn} do
187 other_user = insert(:user)
189 {:ok, user} = User.mute(user, other_user)
193 |> assign(:user, user)
194 |> get("/api/v1/mutes")
196 other_user_id = to_string(other_user.id)
197 assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
200 test "getting a list of blocks", %{conn: conn} do
202 other_user = insert(:user)
204 {:ok, user} = User.block(user, other_user)
208 |> assign(:user, user)
209 |> get("/api/v1/blocks")
211 other_user_id = to_string(other_user.id)
212 assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
215 test "unimplemented follow_requests, blocks, domain blocks" do
218 ["blocks", "domain_blocks", "follow_requests"]
219 |> Enum.each(fn endpoint ->
222 |> assign(:user, user)
223 |> get("/api/v1/#{endpoint}")
225 assert [] = json_response(conn, 200)
229 test "returns the favorites of a user", %{conn: conn} do
231 other_user = insert(:user)
233 {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
234 {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
236 {:ok, _, _} = CommonAPI.favorite(activity.id, user)
240 |> assign(:user, user)
241 |> get("/api/v1/favourites")
243 assert [status] = json_response(first_conn, 200)
244 assert status["id"] == to_string(activity.id)
246 assert [{"link", _link_header}] =
247 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
249 # Honours query params
250 {:ok, second_activity} =
251 CommonAPI.post(other_user, %{
253 "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
256 {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
258 last_like = status["id"]
262 |> assign(:user, user)
263 |> get("/api/v1/favourites?since_id=#{last_like}")
265 assert [second_status] = json_response(second_conn, 200)
266 assert second_status["id"] == to_string(second_activity.id)
270 |> assign(:user, user)
271 |> get("/api/v1/favourites?limit=0")
273 assert [] = json_response(third_conn, 200)
276 test "get instance information", %{conn: conn} do
277 conn = get(conn, "/api/v1/instance")
278 assert result = json_response(conn, 200)
280 email = Config.get([:instance, :email])
281 # Note: not checking for "max_toot_chars" since it's optional
287 "email" => from_config_email,
294 "registrations" => _,
298 assert email == from_config_email
301 test "get instance stats", %{conn: conn} do
302 user = insert(:user, %{local: true})
304 user2 = insert(:user, %{local: true})
305 {:ok, _user2} = User.deactivate(user2, !user2.info.deactivated)
307 insert(:user, %{local: false, nickname: "u@peer1.com"})
308 insert(:user, %{local: false, nickname: "u@peer2.com"})
310 {:ok, _} = CommonAPI.post(user, %{"status" => "cofe"})
312 # Stats should count users with missing or nil `info.deactivated` value
316 |> User.get_cached_by_id()
317 |> User.update_info(&Changeset.change(&1, %{deactivated: nil}))
319 Pleroma.Stats.force_update()
321 conn = get(conn, "/api/v1/instance")
323 assert result = json_response(conn, 200)
325 stats = result["stats"]
328 assert stats["user_count"] == 1
329 assert stats["status_count"] == 1
330 assert stats["domain_count"] == 2
333 test "get peers", %{conn: conn} do
334 insert(:user, %{local: false, nickname: "u@peer1.com"})
335 insert(:user, %{local: false, nickname: "u@peer2.com"})
337 Pleroma.Stats.force_update()
339 conn = get(conn, "/api/v1/instance/peers")
341 assert result = json_response(conn, 200)
343 assert ["peer1.com", "peer2.com"] == Enum.sort(result)
346 test "put settings", %{conn: conn} do
351 |> assign(:user, user)
352 |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}})
354 assert _result = json_response(conn, 200)
356 user = User.get_cached_by_ap_id(user.ap_id)
357 assert user.info.settings == %{"programming" => "socks"}
360 describe "link headers" do
361 test "preserves parameters in link headers", %{conn: conn} do
363 other_user = insert(:user)
366 CommonAPI.post(other_user, %{
367 "status" => "hi @#{user.nickname}",
368 "visibility" => "public"
372 CommonAPI.post(other_user, %{
373 "status" => "hi @#{user.nickname}",
374 "visibility" => "public"
377 notification1 = Repo.get_by(Notification, activity_id: activity1.id)
378 notification2 = Repo.get_by(Notification, activity_id: activity2.id)
382 |> assign(:user, user)
383 |> get("/api/v1/notifications", %{media_only: true})
385 assert [link_header] = get_resp_header(conn, "link")
386 assert link_header =~ ~r/media_only=true/
387 assert link_header =~ ~r/min_id=#{notification2.id}/
388 assert link_header =~ ~r/max_id=#{notification1.id}/
392 describe "custom emoji" do
393 test "with tags", %{conn: conn} do
396 |> get("/api/v1/custom_emojis")
397 |> json_response(200)
399 assert Map.has_key?(emoji, "shortcode")
400 assert Map.has_key?(emoji, "static_url")
401 assert Map.has_key?(emoji, "tags")
402 assert is_list(emoji["tags"])
403 assert Map.has_key?(emoji, "category")
404 assert Map.has_key?(emoji, "url")
405 assert Map.has_key?(emoji, "visible_in_picker")
409 describe "index/2 redirections" do
410 setup %{conn: conn} do
414 signing_salt: "cooldude"
419 |> Plug.Session.call(Plug.Session.init(session_opts))
422 test_path = "/web/statuses/test"
423 %{conn: conn, path: test_path}
426 test "redirects not logged-in users to the login page", %{conn: conn, path: path} do
427 conn = get(conn, path)
429 assert conn.status == 302
430 assert redirected_to(conn) == "/web/login"
433 test "redirects not logged-in users to the login page on private instances", %{
437 Config.put([:instance, :public], false)
439 conn = get(conn, path)
441 assert conn.status == 302
442 assert redirected_to(conn) == "/web/login"
445 test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
446 token = insert(:oauth_token)
450 |> assign(:user, token.user)
451 |> put_session(:oauth_token, token.token)
454 assert conn.status == 200
457 test "saves referer path to session", %{conn: conn, path: path} do
458 conn = get(conn, path)
459 return_to = Plug.Conn.get_session(conn, :return_to)
461 assert return_to == path
464 test "redirects to the saved path after log in", %{conn: conn, path: path} do
465 app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
466 auth = insert(:oauth_authorization, app: app)
470 |> put_session(:return_to, path)
471 |> get("/web/login", %{code: auth.token})
473 assert conn.status == 302
474 assert redirected_to(conn) == path
477 test "redirects to the getting-started page when referer is not present", %{conn: conn} do
478 app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
479 auth = insert(:oauth_authorization, app: app)
481 conn = get(conn, "/web/login", %{code: auth.token})
483 assert conn.status == 302
484 assert redirected_to(conn) == "/web/getting-started"
488 describe "GET /api/v1/polls/:id" do
489 test "returns poll entity for object id", %{conn: conn} do
493 CommonAPI.post(user, %{
494 "status" => "Pleroma does",
495 "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
498 object = Object.normalize(activity)
502 |> assign(:user, user)
503 |> get("/api/v1/polls/#{object.id}")
505 response = json_response(conn, 200)
506 id = to_string(object.id)
507 assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
510 test "does not expose polls for private statuses", %{conn: conn} do
512 other_user = insert(:user)
515 CommonAPI.post(user, %{
516 "status" => "Pleroma does",
517 "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
518 "visibility" => "private"
521 object = Object.normalize(activity)
525 |> assign(:user, other_user)
526 |> get("/api/v1/polls/#{object.id}")
528 assert json_response(conn, 404)
532 describe "POST /api/v1/polls/:id/votes" do
533 test "votes are added to the poll", %{conn: conn} do
535 other_user = insert(:user)
538 CommonAPI.post(user, %{
539 "status" => "A very delicious sandwich",
541 "options" => ["Lettuce", "Grilled Bacon", "Tomato"],
547 object = Object.normalize(activity)
551 |> assign(:user, other_user)
552 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
554 assert json_response(conn, 200)
555 object = Object.get_by_id(object.id)
557 assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
562 test "author can't vote", %{conn: conn} do
566 CommonAPI.post(user, %{
567 "status" => "Am I cute?",
568 "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
571 object = Object.normalize(activity)
574 |> assign(:user, user)
575 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
576 |> json_response(422) == %{"error" => "Poll's author can't vote"}
578 object = Object.get_by_id(object.id)
580 refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
583 test "does not allow multiple choices on a single-choice question", %{conn: conn} do
585 other_user = insert(:user)
588 CommonAPI.post(user, %{
589 "status" => "The glass is",
590 "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
593 object = Object.normalize(activity)
596 |> assign(:user, other_user)
597 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
598 |> json_response(422) == %{"error" => "Too many choices"}
600 object = Object.get_by_id(object.id)
602 refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
607 test "does not allow choice index to be greater than options count", %{conn: conn} do
609 other_user = insert(:user)
612 CommonAPI.post(user, %{
613 "status" => "Am I cute?",
614 "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
617 object = Object.normalize(activity)
621 |> assign(:user, other_user)
622 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
624 assert json_response(conn, 422) == %{"error" => "Invalid indices"}
627 test "returns 404 error when object is not exist", %{conn: conn} do
632 |> assign(:user, user)
633 |> post("/api/v1/polls/1/votes", %{"choices" => [0]})
635 assert json_response(conn, 404) == %{"error" => "Record not found"}
638 test "returns 404 when poll is private and not available for user", %{conn: conn} do
640 other_user = insert(:user)
643 CommonAPI.post(user, %{
644 "status" => "Am I cute?",
645 "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
646 "visibility" => "private"
649 object = Object.normalize(activity)
653 |> assign(:user, other_user)
654 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
656 assert json_response(conn, 404) == %{"error" => "Record not found"}
660 describe "POST /auth/password, with valid parameters" do
661 setup %{conn: conn} do
663 conn = post(conn, "/auth/password?email=#{user.email}")
664 %{conn: conn, user: user}
667 test "it returns 204", %{conn: conn} do
668 assert json_response(conn, :no_content)
671 test "it creates a PasswordResetToken record for user", %{user: user} do
672 token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
676 test "it sends an email to user", %{user: user} do
677 ObanHelpers.perform_all()
678 token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
680 email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
681 notify_email = Config.get([:instance, :notify_email])
682 instance_name = Config.get([:instance, :name])
685 from: {instance_name, notify_email},
686 to: {user.name, user.email},
687 html_body: email.html_body
692 describe "POST /auth/password, with invalid parameters" do
698 test "it returns 404 when user is not found", %{conn: conn, user: user} do
699 conn = post(conn, "/auth/password?email=nonexisting_#{user.email}")
700 assert conn.status == 404
701 assert conn.resp_body == ""
704 test "it returns 400 when user is not local", %{conn: conn, user: user} do
705 {:ok, user} = Repo.update(Changeset.change(user, local: false))
706 conn = post(conn, "/auth/password?email=#{user.email}")
707 assert conn.status == 400
708 assert conn.resp_body == ""
712 describe "GET /api/v1/suggestions" do
715 other_user = insert(:user)
716 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
717 url500 = "http://test500?#{host}&#{user.nickname}"
718 url200 = "http://test200?#{host}&#{user.nickname}"
721 %{method: :get, url: ^url500} ->
722 %Tesla.Env{status: 500, body: "bad request"}
724 %{method: :get, url: ^url200} ->
728 ~s([{"acct":"yj455","avatar":"https://social.heldscal.la/avatar/201.jpeg","avatar_static":"https://social.heldscal.la/avatar/s/201.jpeg"}, {"acct":"#{
730 }","avatar":"https://social.heldscal.la/avatar/202.jpeg","avatar_static":"https://social.heldscal.la/avatar/s/202.jpeg"}])
734 [user: user, other_user: other_user]
737 clear_config(:suggestions)
739 test "returns empty result when suggestions disabled", %{conn: conn, user: user} do
740 Config.put([:suggestions, :enabled], false)
744 |> assign(:user, user)
745 |> get("/api/v1/suggestions")
746 |> json_response(200)
751 test "returns error", %{conn: conn, user: user} do
752 Config.put([:suggestions, :enabled], true)
753 Config.put([:suggestions, :third_party_engine], "http://test500?{{host}}&{{user}}")
755 assert capture_log(fn ->
758 |> assign(:user, user)
759 |> get("/api/v1/suggestions")
760 |> json_response(500)
762 assert res == "Something went wrong"
763 end) =~ "Could not retrieve suggestions"
766 test "returns suggestions", %{conn: conn, user: user, other_user: other_user} do
767 Config.put([:suggestions, :enabled], true)
768 Config.put([:suggestions, :third_party_engine], "http://test200?{{host}}&{{user}}")
772 |> assign(:user, user)
773 |> get("/api/v1/suggestions")
774 |> json_response(200)
779 "avatar" => "https://social.heldscal.la/avatar/201.jpeg",
780 "avatar_static" => "https://social.heldscal.la/avatar/s/201.jpeg",
784 "acct" => other_user.ap_id,
785 "avatar" => "https://social.heldscal.la/avatar/202.jpeg",
786 "avatar_static" => "https://social.heldscal.la/avatar/s/202.jpeg",
787 "id" => other_user.id
793 describe "PUT /api/v1/media/:id" do
795 actor = insert(:user)
798 content_type: "image/jpg",
799 path: Path.absname("test/fixtures/image.jpg"),
800 filename: "an_image.jpg"
803 {:ok, %Object{} = object} =
806 actor: User.ap_id(actor),
807 description: "test-m"
810 [actor: actor, object: object]
813 test "updates name of media", %{conn: conn, actor: actor, object: object} do
816 |> assign(:user, actor)
817 |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"})
818 |> json_response(:ok)
820 assert media["description"] == "test-media"
821 assert refresh_record(object).data["name"] == "test-media"
824 test "returns error wheb request is bad", %{conn: conn, actor: actor, object: object} do
827 |> assign(:user, actor)
828 |> put("/api/v1/media/#{object.id}", %{})
829 |> json_response(400)
831 assert media == %{"error" => "bad_request"}
835 describe "DELETE /auth/sign_out" do
836 test "redirect to root page", %{conn: conn} do
841 |> assign(:user, user)
842 |> delete("/auth/sign_out")
844 assert conn.status == 302
845 assert redirected_to(conn) == "/"
849 describe "empty_array, stubs for mastodon api" do
850 test "GET /api/v1/accounts/:id/identity_proofs", %{conn: conn} do
855 |> assign(:user, user)
856 |> get("/api/v1/accounts/#{user.id}/identity_proofs")
857 |> json_response(200)
862 test "GET /api/v1/endorsements", %{conn: conn} do
867 |> assign(:user, user)
868 |> get("/api/v1/endorsements")
869 |> json_response(200)
874 test "GET /api/v1/trends", %{conn: conn} do
879 |> assign(:user, user)
880 |> get("/api/v1/trends")
881 |> json_response(200)