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 test "getting a list of mutes", %{conn: conn} do
119 other_user = insert(:user)
121 {:ok, user} = User.mute(user, other_user)
125 |> assign(:user, user)
126 |> get("/api/v1/mutes")
128 other_user_id = to_string(other_user.id)
129 assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
132 test "getting a list of blocks", %{conn: conn} do
134 other_user = insert(:user)
136 {:ok, user} = User.block(user, other_user)
140 |> assign(:user, user)
141 |> get("/api/v1/blocks")
143 other_user_id = to_string(other_user.id)
144 assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
147 test "unimplemented follow_requests, blocks, domain blocks" do
150 ["blocks", "domain_blocks", "follow_requests"]
151 |> Enum.each(fn endpoint ->
154 |> assign(:user, user)
155 |> get("/api/v1/#{endpoint}")
157 assert [] = json_response(conn, 200)
161 test "returns the favorites of a user", %{conn: conn} do
163 other_user = insert(:user)
165 {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
166 {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
168 {:ok, _, _} = CommonAPI.favorite(activity.id, user)
172 |> assign(:user, user)
173 |> get("/api/v1/favourites")
175 assert [status] = json_response(first_conn, 200)
176 assert status["id"] == to_string(activity.id)
178 assert [{"link", _link_header}] =
179 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
181 # Honours query params
182 {:ok, second_activity} =
183 CommonAPI.post(other_user, %{
185 "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
188 {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
190 last_like = status["id"]
194 |> assign(:user, user)
195 |> get("/api/v1/favourites?since_id=#{last_like}")
197 assert [second_status] = json_response(second_conn, 200)
198 assert second_status["id"] == to_string(second_activity.id)
202 |> assign(:user, user)
203 |> get("/api/v1/favourites?limit=0")
205 assert [] = json_response(third_conn, 200)
208 test "get instance information", %{conn: conn} do
209 conn = get(conn, "/api/v1/instance")
210 assert result = json_response(conn, 200)
212 email = Config.get([:instance, :email])
213 # Note: not checking for "max_toot_chars" since it's optional
219 "email" => from_config_email,
226 "registrations" => _,
230 assert email == from_config_email
233 test "get instance stats", %{conn: conn} do
234 user = insert(:user, %{local: true})
236 user2 = insert(:user, %{local: true})
237 {:ok, _user2} = User.deactivate(user2, !user2.info.deactivated)
239 insert(:user, %{local: false, nickname: "u@peer1.com"})
240 insert(:user, %{local: false, nickname: "u@peer2.com"})
242 {:ok, _} = CommonAPI.post(user, %{"status" => "cofe"})
244 # Stats should count users with missing or nil `info.deactivated` value
248 |> User.get_cached_by_id()
249 |> User.update_info(&Changeset.change(&1, %{deactivated: nil}))
251 Pleroma.Stats.force_update()
253 conn = get(conn, "/api/v1/instance")
255 assert result = json_response(conn, 200)
257 stats = result["stats"]
260 assert stats["user_count"] == 1
261 assert stats["status_count"] == 1
262 assert stats["domain_count"] == 2
265 test "get peers", %{conn: conn} do
266 insert(:user, %{local: false, nickname: "u@peer1.com"})
267 insert(:user, %{local: false, nickname: "u@peer2.com"})
269 Pleroma.Stats.force_update()
271 conn = get(conn, "/api/v1/instance/peers")
273 assert result = json_response(conn, 200)
275 assert ["peer1.com", "peer2.com"] == Enum.sort(result)
278 test "put settings", %{conn: conn} do
283 |> assign(:user, user)
284 |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}})
286 assert _result = json_response(conn, 200)
288 user = User.get_cached_by_ap_id(user.ap_id)
289 assert user.info.settings == %{"programming" => "socks"}
292 describe "link headers" do
293 test "preserves parameters in link headers", %{conn: conn} do
295 other_user = insert(:user)
298 CommonAPI.post(other_user, %{
299 "status" => "hi @#{user.nickname}",
300 "visibility" => "public"
304 CommonAPI.post(other_user, %{
305 "status" => "hi @#{user.nickname}",
306 "visibility" => "public"
309 notification1 = Repo.get_by(Notification, activity_id: activity1.id)
310 notification2 = Repo.get_by(Notification, activity_id: activity2.id)
314 |> assign(:user, user)
315 |> get("/api/v1/notifications", %{media_only: true})
317 assert [link_header] = get_resp_header(conn, "link")
318 assert link_header =~ ~r/media_only=true/
319 assert link_header =~ ~r/min_id=#{notification2.id}/
320 assert link_header =~ ~r/max_id=#{notification1.id}/
324 describe "custom emoji" do
325 test "with tags", %{conn: conn} do
328 |> get("/api/v1/custom_emojis")
329 |> json_response(200)
331 assert Map.has_key?(emoji, "shortcode")
332 assert Map.has_key?(emoji, "static_url")
333 assert Map.has_key?(emoji, "tags")
334 assert is_list(emoji["tags"])
335 assert Map.has_key?(emoji, "category")
336 assert Map.has_key?(emoji, "url")
337 assert Map.has_key?(emoji, "visible_in_picker")
341 describe "index/2 redirections" do
342 setup %{conn: conn} do
346 signing_salt: "cooldude"
351 |> Plug.Session.call(Plug.Session.init(session_opts))
354 test_path = "/web/statuses/test"
355 %{conn: conn, path: test_path}
358 test "redirects not logged-in users to the login page", %{conn: conn, path: path} do
359 conn = get(conn, path)
361 assert conn.status == 302
362 assert redirected_to(conn) == "/web/login"
365 test "redirects not logged-in users to the login page on private instances", %{
369 Config.put([:instance, :public], false)
371 conn = get(conn, path)
373 assert conn.status == 302
374 assert redirected_to(conn) == "/web/login"
377 test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
378 token = insert(:oauth_token)
382 |> assign(:user, token.user)
383 |> put_session(:oauth_token, token.token)
386 assert conn.status == 200
389 test "saves referer path to session", %{conn: conn, path: path} do
390 conn = get(conn, path)
391 return_to = Plug.Conn.get_session(conn, :return_to)
393 assert return_to == path
396 test "redirects to the saved path after log in", %{conn: conn, path: path} do
397 app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
398 auth = insert(:oauth_authorization, app: app)
402 |> put_session(:return_to, path)
403 |> get("/web/login", %{code: auth.token})
405 assert conn.status == 302
406 assert redirected_to(conn) == path
409 test "redirects to the getting-started page when referer is not present", %{conn: conn} do
410 app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".")
411 auth = insert(:oauth_authorization, app: app)
413 conn = get(conn, "/web/login", %{code: auth.token})
415 assert conn.status == 302
416 assert redirected_to(conn) == "/web/getting-started"
420 describe "GET /api/v1/polls/:id" do
421 test "returns poll entity for object id", %{conn: conn} do
425 CommonAPI.post(user, %{
426 "status" => "Pleroma does",
427 "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
430 object = Object.normalize(activity)
434 |> assign(:user, user)
435 |> get("/api/v1/polls/#{object.id}")
437 response = json_response(conn, 200)
438 id = to_string(object.id)
439 assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
442 test "does not expose polls for private statuses", %{conn: conn} do
444 other_user = insert(:user)
447 CommonAPI.post(user, %{
448 "status" => "Pleroma does",
449 "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
450 "visibility" => "private"
453 object = Object.normalize(activity)
457 |> assign(:user, other_user)
458 |> get("/api/v1/polls/#{object.id}")
460 assert json_response(conn, 404)
464 describe "POST /api/v1/polls/:id/votes" do
465 test "votes are added to the poll", %{conn: conn} do
467 other_user = insert(:user)
470 CommonAPI.post(user, %{
471 "status" => "A very delicious sandwich",
473 "options" => ["Lettuce", "Grilled Bacon", "Tomato"],
479 object = Object.normalize(activity)
483 |> assign(:user, other_user)
484 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
486 assert json_response(conn, 200)
487 object = Object.get_by_id(object.id)
489 assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
494 test "author can't vote", %{conn: conn} do
498 CommonAPI.post(user, %{
499 "status" => "Am I cute?",
500 "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
503 object = Object.normalize(activity)
506 |> assign(:user, user)
507 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
508 |> json_response(422) == %{"error" => "Poll's author can't vote"}
510 object = Object.get_by_id(object.id)
512 refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
515 test "does not allow multiple choices on a single-choice question", %{conn: conn} do
517 other_user = insert(:user)
520 CommonAPI.post(user, %{
521 "status" => "The glass is",
522 "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
525 object = Object.normalize(activity)
528 |> assign(:user, other_user)
529 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
530 |> json_response(422) == %{"error" => "Too many choices"}
532 object = Object.get_by_id(object.id)
534 refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
539 test "does not allow choice index to be greater than options count", %{conn: conn} do
541 other_user = insert(:user)
544 CommonAPI.post(user, %{
545 "status" => "Am I cute?",
546 "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
549 object = Object.normalize(activity)
553 |> assign(:user, other_user)
554 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
556 assert json_response(conn, 422) == %{"error" => "Invalid indices"}
559 test "returns 404 error when object is not exist", %{conn: conn} do
564 |> assign(:user, user)
565 |> post("/api/v1/polls/1/votes", %{"choices" => [0]})
567 assert json_response(conn, 404) == %{"error" => "Record not found"}
570 test "returns 404 when poll is private and not available for user", %{conn: conn} do
572 other_user = insert(:user)
575 CommonAPI.post(user, %{
576 "status" => "Am I cute?",
577 "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
578 "visibility" => "private"
581 object = Object.normalize(activity)
585 |> assign(:user, other_user)
586 |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
588 assert json_response(conn, 404) == %{"error" => "Record not found"}
592 describe "POST /auth/password, with valid parameters" do
593 setup %{conn: conn} do
595 conn = post(conn, "/auth/password?email=#{user.email}")
596 %{conn: conn, user: user}
599 test "it returns 204", %{conn: conn} do
600 assert json_response(conn, :no_content)
603 test "it creates a PasswordResetToken record for user", %{user: user} do
604 token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
608 test "it sends an email to user", %{user: user} do
609 ObanHelpers.perform_all()
610 token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
612 email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
613 notify_email = Config.get([:instance, :notify_email])
614 instance_name = Config.get([:instance, :name])
617 from: {instance_name, notify_email},
618 to: {user.name, user.email},
619 html_body: email.html_body
624 describe "POST /auth/password, with invalid parameters" do
630 test "it returns 404 when user is not found", %{conn: conn, user: user} do
631 conn = post(conn, "/auth/password?email=nonexisting_#{user.email}")
632 assert conn.status == 404
633 assert conn.resp_body == ""
636 test "it returns 400 when user is not local", %{conn: conn, user: user} do
637 {:ok, user} = Repo.update(Changeset.change(user, local: false))
638 conn = post(conn, "/auth/password?email=#{user.email}")
639 assert conn.status == 400
640 assert conn.resp_body == ""
644 describe "GET /api/v1/suggestions" do
647 other_user = insert(:user)
648 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
649 url500 = "http://test500?#{host}&#{user.nickname}"
650 url200 = "http://test200?#{host}&#{user.nickname}"
653 %{method: :get, url: ^url500} ->
654 %Tesla.Env{status: 500, body: "bad request"}
656 %{method: :get, url: ^url200} ->
660 ~s([{"acct":"yj455","avatar":"https://social.heldscal.la/avatar/201.jpeg","avatar_static":"https://social.heldscal.la/avatar/s/201.jpeg"}, {"acct":"#{
662 }","avatar":"https://social.heldscal.la/avatar/202.jpeg","avatar_static":"https://social.heldscal.la/avatar/s/202.jpeg"}])
666 [user: user, other_user: other_user]
669 clear_config(:suggestions)
671 test "returns empty result when suggestions disabled", %{conn: conn, user: user} do
672 Config.put([:suggestions, :enabled], false)
676 |> assign(:user, user)
677 |> get("/api/v1/suggestions")
678 |> json_response(200)
683 test "returns error", %{conn: conn, user: user} do
684 Config.put([:suggestions, :enabled], true)
685 Config.put([:suggestions, :third_party_engine], "http://test500?{{host}}&{{user}}")
687 assert capture_log(fn ->
690 |> assign(:user, user)
691 |> get("/api/v1/suggestions")
692 |> json_response(500)
694 assert res == "Something went wrong"
695 end) =~ "Could not retrieve suggestions"
698 test "returns suggestions", %{conn: conn, user: user, other_user: other_user} do
699 Config.put([:suggestions, :enabled], true)
700 Config.put([:suggestions, :third_party_engine], "http://test200?{{host}}&{{user}}")
704 |> assign(:user, user)
705 |> get("/api/v1/suggestions")
706 |> json_response(200)
711 "avatar" => "https://social.heldscal.la/avatar/201.jpeg",
712 "avatar_static" => "https://social.heldscal.la/avatar/s/201.jpeg",
716 "acct" => other_user.ap_id,
717 "avatar" => "https://social.heldscal.la/avatar/202.jpeg",
718 "avatar_static" => "https://social.heldscal.la/avatar/s/202.jpeg",
719 "id" => other_user.id
725 describe "PUT /api/v1/media/:id" do
727 actor = insert(:user)
730 content_type: "image/jpg",
731 path: Path.absname("test/fixtures/image.jpg"),
732 filename: "an_image.jpg"
735 {:ok, %Object{} = object} =
738 actor: User.ap_id(actor),
739 description: "test-m"
742 [actor: actor, object: object]
745 test "updates name of media", %{conn: conn, actor: actor, object: object} do
748 |> assign(:user, actor)
749 |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"})
750 |> json_response(:ok)
752 assert media["description"] == "test-media"
753 assert refresh_record(object).data["name"] == "test-media"
756 test "returns error wheb request is bad", %{conn: conn, actor: actor, object: object} do
759 |> assign(:user, actor)
760 |> put("/api/v1/media/#{object.id}", %{})
761 |> json_response(400)
763 assert media == %{"error" => "bad_request"}
767 describe "DELETE /auth/sign_out" do
768 test "redirect to root page", %{conn: conn} do
773 |> assign(:user, user)
774 |> delete("/auth/sign_out")
776 assert conn.status == 302
777 assert redirected_to(conn) == "/"
781 describe "empty_array, stubs for mastodon api" do
782 test "GET /api/v1/accounts/:id/identity_proofs", %{conn: conn} do
787 |> assign(:user, user)
788 |> get("/api/v1/accounts/#{user.id}/identity_proofs")
789 |> json_response(200)
794 test "GET /api/v1/endorsements", %{conn: conn} do
799 |> assign(:user, user)
800 |> get("/api/v1/endorsements")
801 |> json_response(200)
806 test "GET /api/v1/trends", %{conn: conn} do
811 |> assign(:user, user)
812 |> get("/api/v1/trends")
813 |> json_response(200)