1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
6 use Pleroma.Web.ConnCase
12 alias Pleroma.Web.CommonAPI
15 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
20 setup do: oauth_access(["read:statuses"])
22 test "does NOT embed account/pleroma/relationship in statuses", %{
26 other_user = insert(:user)
28 {:ok, _} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
32 |> assign(:user, user)
33 |> get("/api/v1/timelines/home")
34 |> json_response_and_validate_schema(200)
36 assert Enum.all?(response, fn n ->
37 get_in(n, ["account", "pleroma", "relationship"]) == %{}
41 test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do
42 {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
43 {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
45 {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
47 {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"})
49 conn = get(conn, "/api/v1/timelines/home?exclude_visibilities[]=direct")
51 assert status_ids = json_response_and_validate_schema(conn, :ok) |> Enum.map(& &1["id"])
52 assert public_activity.id in status_ids
53 assert unlisted_activity.id in status_ids
54 assert private_activity.id in status_ids
55 refute direct_activity.id in status_ids
58 test "muted emotions", %{user: user, conn: conn} do
59 other_user = insert(:user)
60 {:ok, activity} = CommonAPI.post(user, %{status: "."})
62 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
63 User.mute(user, other_user)
67 |> assign(:user, user)
68 |> get("/api/v1/timelines/home")
69 |> json_response_and_validate_schema(200)
74 "emoji_reactions" => []
81 |> assign(:user, user)
82 |> get("/api/v1/timelines/home?with_muted=true")
83 |> json_response_and_validate_schema(200)
88 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
94 test "local/remote filtering", %{conn: conn, user: user} do
96 remote = insert(:user, local: false)
98 {:ok, user, local} = User.follow(user, local)
99 {:ok, _user, remote} = User.follow(user, remote)
104 "to" => ["https://www.w3.org/ns/activitystreams#Public", User.ap_followers(local)]
110 insert(:note_activity, %{
112 recipients: ["https://www.w3.org/ns/activitystreams#Public", User.ap_followers(local)],
119 "to" => ["https://www.w3.org/ns/activitystreams#Public", User.ap_followers(remote)]
125 insert(:note_activity, %{
127 recipients: ["https://www.w3.org/ns/activitystreams#Public", User.ap_followers(remote)],
134 |> get("/api/v1/timelines/home")
135 |> json_response_and_validate_schema(200)
137 without_filter_ids = Enum.map(resp1, & &1["id"])
139 assert activity1.id in without_filter_ids
140 assert activity2.id in without_filter_ids
144 |> get("/api/v1/timelines/home?local=true")
145 |> json_response_and_validate_schema(200)
147 only_local_ids = Enum.map(resp2, & &1["id"])
149 assert activity1.id in only_local_ids
150 refute activity2.id in only_local_ids
154 |> get("/api/v1/timelines/home?only_remote=true")
155 |> json_response_and_validate_schema(200)
157 only_remote_ids = Enum.map(resp3, & &1["id"])
159 refute activity1.id in only_remote_ids
160 assert activity2.id in only_remote_ids
164 |> get("/api/v1/timelines/home?only_remote=true&local=true")
165 |> json_response_and_validate_schema(200)
172 @tag capture_log: true
173 test "the public timeline", %{conn: conn} do
176 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
178 _activity = insert(:note_activity, local: false)
180 conn = get(conn, "/api/v1/timelines/public?local=False")
182 assert length(json_response_and_validate_schema(conn, :ok)) == 2
184 conn = get(build_conn(), "/api/v1/timelines/public?local=True")
186 assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok)
188 conn = get(build_conn(), "/api/v1/timelines/public?local=1")
190 assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok)
192 # does not contain repeats
193 {:ok, _} = CommonAPI.repeat(activity.id, user)
195 conn = get(build_conn(), "/api/v1/timelines/public?local=true")
197 assert [_] = json_response_and_validate_schema(conn, :ok)
200 test "the public timeline includes only public statuses for an authenticated user" do
201 %{user: user, conn: conn} = oauth_access(["read:statuses"])
203 {:ok, _activity} = CommonAPI.post(user, %{status: "test"})
204 {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "private"})
205 {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "unlisted"})
206 {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
208 res_conn = get(conn, "/api/v1/timelines/public")
209 assert length(json_response_and_validate_schema(res_conn, 200)) == 1
212 test "doesn't return replies if follower is posting with blocked user" do
213 %{conn: conn, user: blocker} = oauth_access(["read:statuses"])
214 [blockee, friend] = insert_list(2, :user)
215 {:ok, blocker, friend} = User.follow(blocker, friend)
216 {:ok, _} = User.block(blocker, blockee)
218 conn = assign(conn, :user, blocker)
220 {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})
222 {:ok, reply_from_blockee} =
223 CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})
225 {:ok, _reply_from_friend} =
226 CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})
228 # Still shows replies from yourself
229 {:ok, %{id: reply_from_me}} =
230 CommonAPI.post(blocker, %{status: "status", in_reply_to_status_id: reply_from_blockee})
233 get(conn, "/api/v1/timelines/public")
234 |> json_response_and_validate_schema(200)
236 assert length(response) == 2
237 [%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response
240 test "doesn't return replies if follow is posting with users from blocked domain" do
241 %{conn: conn, user: blocker} = oauth_access(["read:statuses"])
242 friend = insert(:user)
243 blockee = insert(:user, ap_id: "https://example.com/users/blocked")
244 {:ok, blocker, friend} = User.follow(blocker, friend)
245 {:ok, blocker} = User.block_domain(blocker, "example.com")
247 conn = assign(conn, :user, blocker)
249 {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})
251 {:ok, reply_from_blockee} =
252 CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})
254 {:ok, _reply_from_friend} =
255 CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})
257 res_conn = get(conn, "/api/v1/timelines/public")
259 activities = json_response_and_validate_schema(res_conn, 200)
260 [%{"id" => ^activity_id}] = activities
263 test "can be filtered by instance", %{conn: conn} do
264 user = insert(:user, ap_id: "https://lain.com/users/lain")
265 insert(:note_activity, local: false)
266 insert(:note_activity, local: false)
268 {:ok, _} = CommonAPI.post(user, %{status: "test"})
270 conn = get(conn, "/api/v1/timelines/public?instance=lain.com")
272 assert length(json_response_and_validate_schema(conn, :ok)) == 1
275 test "muted emotions", %{conn: conn} do
277 token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
281 |> assign(:user, user)
282 |> assign(:token, token)
284 other_user = insert(:user)
285 {:ok, activity} = CommonAPI.post(user, %{status: "."})
287 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
288 User.mute(user, other_user)
292 |> get("/api/v1/timelines/public")
293 |> json_response_and_validate_schema(200)
298 "emoji_reactions" => []
305 |> get("/api/v1/timelines/public?with_muted=true")
306 |> json_response_and_validate_schema(200)
311 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
318 defp local_and_remote_activities do
319 insert(:note_activity)
320 insert(:note_activity, local: false)
324 describe "public with restrict unauthenticated timeline for local and federated timelines" do
325 setup do: local_and_remote_activities()
327 setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true)
329 setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true)
331 test "if user is unauthenticated", %{conn: conn} do
332 res_conn = get(conn, "/api/v1/timelines/public?local=true")
334 assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
335 "error" => "authorization required for timeline view"
338 res_conn = get(conn, "/api/v1/timelines/public?local=false")
340 assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
341 "error" => "authorization required for timeline view"
345 test "if user is authenticated" do
346 %{conn: conn} = oauth_access(["read:statuses"])
348 res_conn = get(conn, "/api/v1/timelines/public?local=true")
349 assert length(json_response_and_validate_schema(res_conn, 200)) == 1
351 res_conn = get(conn, "/api/v1/timelines/public?local=false")
352 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
356 describe "public with restrict unauthenticated timeline for local" do
357 setup do: local_and_remote_activities()
359 setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true)
361 test "if user is unauthenticated", %{conn: conn} do
362 res_conn = get(conn, "/api/v1/timelines/public?local=true")
364 assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
365 "error" => "authorization required for timeline view"
368 res_conn = get(conn, "/api/v1/timelines/public?local=false")
369 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
372 test "if user is authenticated", %{conn: _conn} do
373 %{conn: conn} = oauth_access(["read:statuses"])
375 res_conn = get(conn, "/api/v1/timelines/public?local=true")
376 assert length(json_response_and_validate_schema(res_conn, 200)) == 1
378 res_conn = get(conn, "/api/v1/timelines/public?local=false")
379 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
383 describe "public with restrict unauthenticated timeline for remote" do
384 setup do: local_and_remote_activities()
386 setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true)
388 test "if user is unauthenticated", %{conn: conn} do
389 res_conn = get(conn, "/api/v1/timelines/public?local=true")
390 assert length(json_response_and_validate_schema(res_conn, 200)) == 1
392 res_conn = get(conn, "/api/v1/timelines/public?local=false")
394 assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
395 "error" => "authorization required for timeline view"
399 test "if user is authenticated", %{conn: _conn} do
400 %{conn: conn} = oauth_access(["read:statuses"])
402 res_conn = get(conn, "/api/v1/timelines/public?local=true")
403 assert length(json_response_and_validate_schema(res_conn, 200)) == 1
405 res_conn = get(conn, "/api/v1/timelines/public?local=false")
406 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
411 test "direct timeline", %{conn: conn} do
412 user_one = insert(:user)
413 user_two = insert(:user)
415 {:ok, user_two, user_one} = User.follow(user_two, user_one)
418 CommonAPI.post(user_one, %{
419 status: "Hi @#{user_two.nickname}!",
423 {:ok, _follower_only} =
424 CommonAPI.post(user_one, %{
425 status: "Hi @#{user_two.nickname}!",
426 visibility: "private"
431 |> assign(:user, user_two)
432 |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"]))
434 # Only direct should be visible here
435 res_conn = get(conn_user_two, "api/v1/timelines/direct")
437 assert [status] = json_response_and_validate_schema(res_conn, :ok)
439 assert %{"visibility" => "direct"} = status
440 assert status["url"] != direct.data["id"]
442 # User should be able to see their own direct message
445 |> assign(:user, user_one)
446 |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"]))
447 |> get("api/v1/timelines/direct")
449 [status] = json_response_and_validate_schema(res_conn, :ok)
451 assert %{"visibility" => "direct"} = status
453 # Both should be visible here
454 res_conn = get(conn_user_two, "api/v1/timelines/home")
456 [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok)
459 Enum.each(1..20, fn _ ->
461 CommonAPI.post(user_one, %{
462 status: "Hi @#{user_two.nickname}!",
467 res_conn = get(conn_user_two, "api/v1/timelines/direct")
469 statuses = json_response_and_validate_schema(res_conn, :ok)
470 assert length(statuses) == 20
472 max_id = List.last(statuses)["id"]
474 res_conn = get(conn_user_two, "api/v1/timelines/direct?max_id=#{max_id}")
476 assert [status] = json_response_and_validate_schema(res_conn, :ok)
478 assert status["url"] != direct.data["id"]
481 test "doesn't include DMs from blocked users" do
482 %{user: blocker, conn: conn} = oauth_access(["read:statuses"])
483 blocked = insert(:user)
484 other_user = insert(:user)
485 {:ok, _user_relationship} = User.block(blocker, blocked)
487 {:ok, _blocked_direct} =
488 CommonAPI.post(blocked, %{
489 status: "Hi @#{blocker.nickname}!",
494 CommonAPI.post(other_user, %{
495 status: "Hi @#{blocker.nickname}!",
499 res_conn = get(conn, "api/v1/timelines/direct")
501 [status] = json_response_and_validate_schema(res_conn, :ok)
502 assert status["id"] == direct.id
507 setup do: oauth_access(["read:lists"])
509 test "does not contain retoots", %{user: user, conn: conn} do
510 other_user = insert(:user)
511 {:ok, activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."})
512 {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."})
513 {:ok, _} = CommonAPI.repeat(activity_one.id, other_user)
515 {:ok, list} = Pleroma.List.create("name", user)
516 {:ok, list} = Pleroma.List.follow(list, other_user)
518 conn = get(conn, "/api/v1/timelines/list/#{list.id}")
520 assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
522 assert id == to_string(activity_two.id)
525 test "works with pagination", %{user: user, conn: conn} do
526 other_user = insert(:user)
527 {:ok, list} = Pleroma.List.create("name", user)
528 {:ok, list} = Pleroma.List.follow(list, other_user)
530 Enum.each(1..30, fn i ->
531 CommonAPI.post(other_user, %{status: "post number #{i}"})
535 get(conn, "/api/v1/timelines/list/#{list.id}?limit=1")
536 |> json_response_and_validate_schema(:ok)
538 assert length(res) == 1
543 get(conn, "/api/v1/timelines/list/#{list.id}?max_id=#{first["id"]}&limit=30")
544 |> json_response_and_validate_schema(:ok)
546 assert length(res) == 29
549 test "list timeline", %{user: user, conn: conn} do
550 other_user = insert(:user)
551 {:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."})
552 {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."})
553 {:ok, list} = Pleroma.List.create("name", user)
554 {:ok, list} = Pleroma.List.follow(list, other_user)
556 conn = get(conn, "/api/v1/timelines/list/#{list.id}")
558 assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
560 assert id == to_string(activity_two.id)
563 test "list timeline does not leak non-public statuses for unfollowed users", %{
567 other_user = insert(:user)
568 {:ok, activity_one} = CommonAPI.post(other_user, %{status: "Marisa is cute."})
570 {:ok, _activity_two} =
571 CommonAPI.post(other_user, %{
572 status: "Marisa is cute.",
573 visibility: "private"
576 {:ok, list} = Pleroma.List.create("name", user)
577 {:ok, list} = Pleroma.List.follow(list, other_user)
579 conn = get(conn, "/api/v1/timelines/list/#{list.id}")
581 assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
583 assert id == to_string(activity_one.id)
586 test "muted emotions", %{user: user, conn: conn} do
587 user2 = insert(:user)
588 user3 = insert(:user)
589 {:ok, activity} = CommonAPI.post(user2, %{status: "."})
591 {:ok, _} = CommonAPI.react_with_emoji(activity.id, user3, "🎅")
592 User.mute(user, user3)
594 {:ok, list} = Pleroma.List.create("name", user)
595 {:ok, list} = Pleroma.List.follow(list, user2)
599 |> get("/api/v1/timelines/list/#{list.id}")
600 |> json_response_and_validate_schema(200)
605 "emoji_reactions" => []
612 |> get("/api/v1/timelines/list/#{list.id}?with_muted=true")
613 |> json_response_and_validate_schema(200)
618 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
625 describe "hashtag" do
626 setup do: oauth_access(["n/a"])
628 @tag capture_log: true
629 test "hashtag timeline", %{conn: conn} do
630 following = insert(:user)
632 {:ok, activity} = CommonAPI.post(following, %{status: "test #2hu"})
634 nconn = get(conn, "/api/v1/timelines/tag/2hu")
636 assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok)
638 assert id == to_string(activity.id)
640 # works for different capitalization too
641 nconn = get(conn, "/api/v1/timelines/tag/2HU")
643 assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok)
645 assert id == to_string(activity.id)
648 test "multi-hashtag timeline", %{conn: conn} do
651 {:ok, activity_test} = CommonAPI.post(user, %{status: "#test"})
652 {:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"})
653 {:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"})
655 any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1")
657 [status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok)
659 assert to_string(activity_test.id) == status_test["id"]
660 assert to_string(activity_test1.id) == status_test1["id"]
661 assert to_string(activity_none.id) == status_none["id"]
663 restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none")
665 assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok)
667 all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none")
669 assert [status_none] == json_response_and_validate_schema(all_test, :ok)
672 test "muted emotions", %{conn: conn} do
674 token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
678 |> assign(:user, user)
679 |> assign(:token, token)
681 other_user = insert(:user)
682 {:ok, activity} = CommonAPI.post(user, %{status: "test #2hu"})
684 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
685 User.mute(user, other_user)
689 |> get("/api/v1/timelines/tag/2hu")
690 |> json_response_and_validate_schema(200)
695 "emoji_reactions" => []
702 |> get("/api/v1/timelines/tag/2hu?with_muted=true")
703 |> json_response_and_validate_schema(200)
708 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
715 describe "hashtag timeline handling of :restrict_unauthenticated setting" do
718 {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"})
719 {:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"})
722 |> Ecto.Changeset.change(%{local: false})
723 |> Pleroma.Repo.update()
725 base_uri = "/api/v1/timelines/tag/tag1"
726 error_response = %{"error" => "authorization required for timeline view"}
728 %{base_uri: base_uri, error_response: error_response}
731 defp ensure_authenticated_access(base_uri) do
732 %{conn: auth_conn} = oauth_access(["read:statuses"])
734 res_conn = get(auth_conn, "#{base_uri}?local=true")
735 assert length(json_response(res_conn, 200)) == 1
737 res_conn = get(auth_conn, "#{base_uri}?local=false")
738 assert length(json_response(res_conn, 200)) == 2
741 test "with default settings on private instances, returns 403 for unauthenticated users", %{
744 error_response: error_response
746 clear_config([:instance, :public], false)
747 clear_config([:restrict_unauthenticated, :timelines])
749 for local <- [true, false] do
750 res_conn = get(conn, "#{base_uri}?local=#{local}")
752 assert json_response(res_conn, :unauthorized) == error_response
755 ensure_authenticated_access(base_uri)
758 test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{
761 error_response: error_response
763 clear_config([:restrict_unauthenticated, :timelines, :local], true)
764 clear_config([:restrict_unauthenticated, :timelines, :federated], true)
766 for local <- [true, false] do
767 res_conn = get(conn, "#{base_uri}?local=#{local}")
769 assert json_response(res_conn, :unauthorized) == error_response
772 ensure_authenticated_access(base_uri)
775 test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline",
776 %{conn: conn, base_uri: base_uri, error_response: error_response} do
777 clear_config([:restrict_unauthenticated, :timelines, :local], false)
778 clear_config([:restrict_unauthenticated, :timelines, :federated], true)
780 res_conn = get(conn, "#{base_uri}?local=true")
781 assert length(json_response(res_conn, 200)) == 1
783 res_conn = get(conn, "#{base_uri}?local=false")
784 assert json_response(res_conn, :unauthorized) == error_response
786 ensure_authenticated_access(base_uri)
789 test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline" <>
790 "(but not to local public activities which are delivered as part of federated timeline)",
791 %{conn: conn, base_uri: base_uri, error_response: error_response} do
792 clear_config([:restrict_unauthenticated, :timelines, :local], true)
793 clear_config([:restrict_unauthenticated, :timelines, :federated], false)
795 res_conn = get(conn, "#{base_uri}?local=true")
796 assert json_response(res_conn, :unauthorized) == error_response
798 # Note: local activities get delivered as part of federated timeline
799 res_conn = get(conn, "#{base_uri}?local=false")
800 assert length(json_response(res_conn, 200)) == 2
802 ensure_authenticated_access(base_uri)