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.ActivityPub.ActivityPubControllerTest do
6 use Pleroma.Web.ConnCase
7 use Oban.Testing, repo: Pleroma.Repo
10 alias Pleroma.Activity
11 alias Pleroma.Delivery
12 alias Pleroma.Instances
14 alias Pleroma.Tests.ObanHelpers
16 alias Pleroma.Web.ActivityPub.ObjectView
17 alias Pleroma.Web.ActivityPub.Relay
18 alias Pleroma.Web.ActivityPub.UserView
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Workers.ReceiverWorker
24 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
28 clear_config_all([:instance, :federating],
29 do: Pleroma.Config.put([:instance, :federating], true)
33 clear_config([:instance, :allow_relay])
35 test "with the relay active, it returns the relay user", %{conn: conn} do
38 |> get(activity_pub_path(conn, :relay))
41 assert res["id"] =~ "/relay"
44 test "with the relay disabled, it returns 404", %{conn: conn} do
45 Pleroma.Config.put([:instance, :allow_relay], false)
48 |> get(activity_pub_path(conn, :relay))
54 describe "/internal/fetch" do
55 test "it returns the internal fetch user", %{conn: conn} do
58 |> get(activity_pub_path(conn, :internal_fetch))
61 assert res["id"] =~ "/fetch"
65 describe "/users/:nickname" do
66 test "it returns a json representation of the user with accept application/json", %{
73 |> put_req_header("accept", "application/json")
74 |> get("/users/#{user.nickname}")
76 user = User.get_cached_by_id(user.id)
78 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
81 test "it returns a json representation of the user with accept application/activity+json", %{
88 |> put_req_header("accept", "application/activity+json")
89 |> get("/users/#{user.nickname}")
91 user = User.get_cached_by_id(user.id)
93 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
96 test "it returns a json representation of the user with accept application/ld+json", %{
105 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
107 |> get("/users/#{user.nickname}")
109 user = User.get_cached_by_id(user.id)
111 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
115 describe "/object/:uuid" do
116 test "it returns a json representation of the object with accept application/json", %{
120 uuid = String.split(note.data["id"], "/") |> List.last()
124 |> put_req_header("accept", "application/json")
125 |> get("/objects/#{uuid}")
127 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
130 test "it returns a json representation of the object with accept application/activity+json",
133 uuid = String.split(note.data["id"], "/") |> List.last()
137 |> put_req_header("accept", "application/activity+json")
138 |> get("/objects/#{uuid}")
140 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
143 test "it returns a json representation of the object with accept application/ld+json", %{
147 uuid = String.split(note.data["id"], "/") |> List.last()
153 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
155 |> get("/objects/#{uuid}")
157 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
160 test "it returns 404 for non-public messages", %{conn: conn} do
161 note = insert(:direct_note)
162 uuid = String.split(note.data["id"], "/") |> List.last()
166 |> put_req_header("accept", "application/activity+json")
167 |> get("/objects/#{uuid}")
169 assert json_response(conn, 404)
172 test "it returns 404 for tombstone objects", %{conn: conn} do
173 tombstone = insert(:tombstone)
174 uuid = String.split(tombstone.data["id"], "/") |> List.last()
178 |> put_req_header("accept", "application/activity+json")
179 |> get("/objects/#{uuid}")
181 assert json_response(conn, 404)
184 test "it caches a response", %{conn: conn} do
186 uuid = String.split(note.data["id"], "/") |> List.last()
190 |> put_req_header("accept", "application/activity+json")
191 |> get("/objects/#{uuid}")
193 assert json_response(conn1, :ok)
194 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
198 |> put_req_header("accept", "application/activity+json")
199 |> get("/objects/#{uuid}")
201 assert json_response(conn1, :ok) == json_response(conn2, :ok)
202 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
205 test "cached purged after object deletion", %{conn: conn} do
207 uuid = String.split(note.data["id"], "/") |> List.last()
211 |> put_req_header("accept", "application/activity+json")
212 |> get("/objects/#{uuid}")
214 assert json_response(conn1, :ok)
215 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
221 |> put_req_header("accept", "application/activity+json")
222 |> get("/objects/#{uuid}")
224 assert "Not found" == json_response(conn2, :not_found)
228 describe "/object/:uuid/likes" do
230 like = insert(:like_activity)
231 like_object_ap_id = Object.normalize(like).data["id"]
238 [id: like.data["id"], uuid: uuid]
241 test "it returns the like activities in a collection", %{conn: conn, id: id, uuid: uuid} do
244 |> put_req_header("accept", "application/activity+json")
245 |> get("/objects/#{uuid}/likes")
246 |> json_response(200)
248 assert List.first(result["first"]["orderedItems"])["id"] == id
249 assert result["type"] == "OrderedCollection"
250 assert result["totalItems"] == 1
251 refute result["first"]["next"]
254 test "it does not crash when page number is exceeded total pages", %{conn: conn, uuid: uuid} do
257 |> put_req_header("accept", "application/activity+json")
258 |> get("/objects/#{uuid}/likes?page=2")
259 |> json_response(200)
261 assert result["type"] == "OrderedCollectionPage"
262 assert result["totalItems"] == 1
263 refute result["next"]
264 assert Enum.empty?(result["orderedItems"])
267 test "it contains the next key when likes count is more than 10", %{conn: conn} do
268 note = insert(:note_activity)
269 insert_list(11, :like_activity, note_activity: note)
273 |> Object.normalize()
281 |> put_req_header("accept", "application/activity+json")
282 |> get("/objects/#{uuid}/likes?page=1")
283 |> json_response(200)
285 assert result["totalItems"] == 11
286 assert length(result["orderedItems"]) == 10
287 assert result["next"]
291 describe "/activities/:uuid" do
292 test "it returns a json representation of the activity", %{conn: conn} do
293 activity = insert(:note_activity)
294 uuid = String.split(activity.data["id"], "/") |> List.last()
298 |> put_req_header("accept", "application/activity+json")
299 |> get("/activities/#{uuid}")
301 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
304 test "it returns 404 for non-public activities", %{conn: conn} do
305 activity = insert(:direct_note_activity)
306 uuid = String.split(activity.data["id"], "/") |> List.last()
310 |> put_req_header("accept", "application/activity+json")
311 |> get("/activities/#{uuid}")
313 assert json_response(conn, 404)
316 test "it caches a response", %{conn: conn} do
317 activity = insert(:note_activity)
318 uuid = String.split(activity.data["id"], "/") |> List.last()
322 |> put_req_header("accept", "application/activity+json")
323 |> get("/activities/#{uuid}")
325 assert json_response(conn1, :ok)
326 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
330 |> put_req_header("accept", "application/activity+json")
331 |> get("/activities/#{uuid}")
333 assert json_response(conn1, :ok) == json_response(conn2, :ok)
334 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
337 test "cached purged after activity deletion", %{conn: conn} do
339 {:ok, activity} = CommonAPI.post(user, %{"status" => "cofe"})
341 uuid = String.split(activity.data["id"], "/") |> List.last()
345 |> put_req_header("accept", "application/activity+json")
346 |> get("/activities/#{uuid}")
348 assert json_response(conn1, :ok)
349 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
351 Activity.delete_by_ap_id(activity.object.data["id"])
355 |> put_req_header("accept", "application/activity+json")
356 |> get("/activities/#{uuid}")
358 assert "Not found" == json_response(conn2, :not_found)
363 test "it inserts an incoming activity into the database", %{conn: conn} do
364 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
368 |> assign(:valid_signature, true)
369 |> put_req_header("content-type", "application/activity+json")
370 |> post("/inbox", data)
372 assert "ok" == json_response(conn, 200)
374 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
375 assert Activity.get_by_ap_id(data["id"])
378 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
379 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
381 sender_url = data["actor"]
382 Instances.set_consistently_unreachable(sender_url)
383 refute Instances.reachable?(sender_url)
387 |> assign(:valid_signature, true)
388 |> put_req_header("content-type", "application/activity+json")
389 |> post("/inbox", data)
391 assert "ok" == json_response(conn, 200)
392 assert Instances.reachable?(sender_url)
396 describe "/users/:nickname/inbox" do
399 File.read!("test/fixtures/mastodon-post-activity.json")
405 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
407 data = Map.put(data, "bcc", [user.ap_id])
411 |> assign(:valid_signature, true)
412 |> put_req_header("content-type", "application/activity+json")
413 |> post("/users/#{user.nickname}/inbox", data)
415 assert "ok" == json_response(conn, 200)
416 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
417 assert Activity.get_by_ap_id(data["id"])
420 test "it accepts messages from actors that are followed by the user", %{
424 recipient = insert(:user)
425 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
427 {:ok, recipient} = User.follow(recipient, actor)
431 |> Map.put("attributedTo", actor.ap_id)
435 |> Map.put("actor", actor.ap_id)
436 |> Map.put("object", object)
440 |> assign(:valid_signature, true)
441 |> put_req_header("content-type", "application/activity+json")
442 |> post("/users/#{recipient.nickname}/inbox", data)
444 assert "ok" == json_response(conn, 200)
445 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
446 assert Activity.get_by_ap_id(data["id"])
449 test "it rejects reads from other users", %{conn: conn} do
451 otheruser = insert(:user)
455 |> assign(:user, otheruser)
456 |> put_req_header("accept", "application/activity+json")
457 |> get("/users/#{user.nickname}/inbox")
459 assert json_response(conn, 403)
462 test "it doesn't crash without an authenticated user", %{conn: conn} do
467 |> put_req_header("accept", "application/activity+json")
468 |> get("/users/#{user.nickname}/inbox")
470 assert json_response(conn, 403)
473 test "it returns a note activity in a collection", %{conn: conn} do
474 note_activity = insert(:direct_note_activity)
475 note_object = Object.normalize(note_activity)
476 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
480 |> assign(:user, user)
481 |> put_req_header("accept", "application/activity+json")
482 |> get("/users/#{user.nickname}/inbox?page=true")
484 assert response(conn, 200) =~ note_object.data["content"]
487 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
489 data = Map.put(data, "bcc", [user.ap_id])
491 sender_host = URI.parse(data["actor"]).host
492 Instances.set_consistently_unreachable(sender_host)
493 refute Instances.reachable?(sender_host)
497 |> assign(:valid_signature, true)
498 |> put_req_header("content-type", "application/activity+json")
499 |> post("/users/#{user.nickname}/inbox", data)
501 assert "ok" == json_response(conn, 200)
502 assert Instances.reachable?(sender_host)
505 test "it removes all follower collections but actor's", %{conn: conn} do
506 [actor, recipient] = insert_pair(:user)
509 File.read!("test/fixtures/activitypub-client-post-activity.json")
512 object = Map.put(data["object"], "attributedTo", actor.ap_id)
516 |> Map.put("id", Utils.generate_object_id())
517 |> Map.put("actor", actor.ap_id)
518 |> Map.put("object", object)
520 recipient.follower_address,
521 actor.follower_address
525 recipient.follower_address,
526 "https://www.w3.org/ns/activitystreams#Public"
530 |> assign(:valid_signature, true)
531 |> put_req_header("content-type", "application/activity+json")
532 |> post("/users/#{recipient.nickname}/inbox", data)
533 |> json_response(200)
535 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
537 activity = Activity.get_by_ap_id(data["id"])
540 assert actor.follower_address in activity.recipients
541 assert actor.follower_address in activity.data["cc"]
543 refute recipient.follower_address in activity.recipients
544 refute recipient.follower_address in activity.data["cc"]
545 refute recipient.follower_address in activity.data["to"]
549 describe "/users/:nickname/outbox" do
550 test "it will not bomb when there is no activity", %{conn: conn} do
555 |> put_req_header("accept", "application/activity+json")
556 |> get("/users/#{user.nickname}/outbox")
558 result = json_response(conn, 200)
559 assert user.ap_id <> "/outbox" == result["id"]
562 test "it returns a note activity in a collection", %{conn: conn} do
563 note_activity = insert(:note_activity)
564 note_object = Object.normalize(note_activity)
565 user = User.get_cached_by_ap_id(note_activity.data["actor"])
569 |> put_req_header("accept", "application/activity+json")
570 |> get("/users/#{user.nickname}/outbox?page=true")
572 assert response(conn, 200) =~ note_object.data["content"]
575 test "it returns an announce activity in a collection", %{conn: conn} do
576 announce_activity = insert(:announce_activity)
577 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
581 |> put_req_header("accept", "application/activity+json")
582 |> get("/users/#{user.nickname}/outbox?page=true")
584 assert response(conn, 200) =~ announce_activity.data["object"]
587 test "it rejects posts from other users", %{conn: conn} do
588 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
590 otheruser = insert(:user)
594 |> assign(:user, otheruser)
595 |> put_req_header("content-type", "application/activity+json")
596 |> post("/users/#{user.nickname}/outbox", data)
598 assert json_response(conn, 403)
601 test "it inserts an incoming create activity into the database", %{conn: conn} do
602 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
607 |> assign(:user, user)
608 |> put_req_header("content-type", "application/activity+json")
609 |> post("/users/#{user.nickname}/outbox", data)
611 result = json_response(conn, 201)
613 assert Activity.get_by_ap_id(result["id"])
616 test "it rejects an incoming activity with bogus type", %{conn: conn} do
617 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
622 |> Map.put("type", "BadType")
626 |> assign(:user, user)
627 |> put_req_header("content-type", "application/activity+json")
628 |> post("/users/#{user.nickname}/outbox", data)
630 assert json_response(conn, 400)
633 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
634 note_activity = insert(:note_activity)
635 note_object = Object.normalize(note_activity)
636 user = User.get_cached_by_ap_id(note_activity.data["actor"])
641 id: note_object.data["id"]
647 |> assign(:user, user)
648 |> put_req_header("content-type", "application/activity+json")
649 |> post("/users/#{user.nickname}/outbox", data)
651 result = json_response(conn, 201)
652 assert Activity.get_by_ap_id(result["id"])
654 assert object = Object.get_by_ap_id(note_object.data["id"])
655 assert object.data["type"] == "Tombstone"
658 test "it rejects delete activity of object from other actor", %{conn: conn} do
659 note_activity = insert(:note_activity)
660 note_object = Object.normalize(note_activity)
666 id: note_object.data["id"]
672 |> assign(:user, user)
673 |> put_req_header("content-type", "application/activity+json")
674 |> post("/users/#{user.nickname}/outbox", data)
676 assert json_response(conn, 400)
679 test "it increases like count when receiving a like action", %{conn: conn} do
680 note_activity = insert(:note_activity)
681 note_object = Object.normalize(note_activity)
682 user = User.get_cached_by_ap_id(note_activity.data["actor"])
687 id: note_object.data["id"]
693 |> assign(:user, user)
694 |> put_req_header("content-type", "application/activity+json")
695 |> post("/users/#{user.nickname}/outbox", data)
697 result = json_response(conn, 201)
698 assert Activity.get_by_ap_id(result["id"])
700 assert object = Object.get_by_ap_id(note_object.data["id"])
701 assert object.data["like_count"] == 1
705 describe "/relay/followers" do
706 test "it returns relay followers", %{conn: conn} do
707 relay_actor = Relay.get_actor()
709 User.follow(user, relay_actor)
713 |> assign(:relay, true)
714 |> get("/relay/followers")
715 |> json_response(200)
717 assert result["first"]["orderedItems"] == [user.ap_id]
721 describe "/relay/following" do
722 test "it returns relay following", %{conn: conn} do
725 |> assign(:relay, true)
726 |> get("/relay/following")
727 |> json_response(200)
729 assert result["first"]["orderedItems"] == []
733 describe "/users/:nickname/followers" do
734 test "it returns the followers in a collection", %{conn: conn} do
736 user_two = insert(:user)
737 User.follow(user, user_two)
741 |> get("/users/#{user_two.nickname}/followers")
742 |> json_response(200)
744 assert result["first"]["orderedItems"] == [user.ap_id]
747 test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
749 user_two = insert(:user, %{info: %{hide_followers: true}})
750 User.follow(user, user_two)
754 |> get("/users/#{user_two.nickname}/followers")
755 |> json_response(200)
757 assert is_binary(result["first"])
760 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
762 user = insert(:user, %{info: %{hide_followers: true}})
766 |> get("/users/#{user.nickname}/followers?page=1")
768 assert result.status == 403
769 assert result.resp_body == ""
772 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
774 user = insert(:user, %{info: %{hide_followers: true}})
775 other_user = insert(:user)
776 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
780 |> assign(:user, user)
781 |> get("/users/#{user.nickname}/followers?page=1")
782 |> json_response(200)
784 assert result["totalItems"] == 1
785 assert result["orderedItems"] == [other_user.ap_id]
788 test "it works for more than 10 users", %{conn: conn} do
791 Enum.each(1..15, fn _ ->
792 other_user = insert(:user)
793 User.follow(other_user, user)
798 |> get("/users/#{user.nickname}/followers")
799 |> json_response(200)
801 assert length(result["first"]["orderedItems"]) == 10
802 assert result["first"]["totalItems"] == 15
803 assert result["totalItems"] == 15
807 |> get("/users/#{user.nickname}/followers?page=2")
808 |> json_response(200)
810 assert length(result["orderedItems"]) == 5
811 assert result["totalItems"] == 15
815 describe "/users/:nickname/following" do
816 test "it returns the following in a collection", %{conn: conn} do
818 user_two = insert(:user)
819 User.follow(user, user_two)
823 |> get("/users/#{user.nickname}/following")
824 |> json_response(200)
826 assert result["first"]["orderedItems"] == [user_two.ap_id]
829 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
830 user = insert(:user, %{info: %{hide_follows: true}})
831 user_two = insert(:user)
832 User.follow(user, user_two)
836 |> get("/users/#{user.nickname}/following")
837 |> json_response(200)
839 assert is_binary(result["first"])
842 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
844 user = insert(:user, %{info: %{hide_follows: true}})
848 |> get("/users/#{user.nickname}/following?page=1")
850 assert result.status == 403
851 assert result.resp_body == ""
854 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
856 user = insert(:user, %{info: %{hide_follows: true}})
857 other_user = insert(:user)
858 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
862 |> assign(:user, user)
863 |> get("/users/#{user.nickname}/following?page=1")
864 |> json_response(200)
866 assert result["totalItems"] == 1
867 assert result["orderedItems"] == [other_user.ap_id]
870 test "it works for more than 10 users", %{conn: conn} do
873 Enum.each(1..15, fn _ ->
874 user = User.get_cached_by_id(user.id)
875 other_user = insert(:user)
876 User.follow(user, other_user)
881 |> get("/users/#{user.nickname}/following")
882 |> json_response(200)
884 assert length(result["first"]["orderedItems"]) == 10
885 assert result["first"]["totalItems"] == 15
886 assert result["totalItems"] == 15
890 |> get("/users/#{user.nickname}/following?page=2")
891 |> json_response(200)
893 assert length(result["orderedItems"]) == 5
894 assert result["totalItems"] == 15
898 describe "delivery tracking" do
899 test "it tracks a signed object fetch", %{conn: conn} do
900 user = insert(:user, local: false)
901 activity = insert(:note_activity)
902 object = Object.normalize(activity)
904 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
907 |> put_req_header("accept", "application/activity+json")
908 |> assign(:user, user)
910 |> json_response(200)
912 assert Delivery.get(object.id, user.id)
915 test "it tracks a signed activity fetch", %{conn: conn} do
916 user = insert(:user, local: false)
917 activity = insert(:note_activity)
918 object = Object.normalize(activity)
920 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
923 |> put_req_header("accept", "application/activity+json")
924 |> assign(:user, user)
925 |> get(activity_path)
926 |> json_response(200)
928 assert Delivery.get(object.id, user.id)
931 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
932 user = insert(:user, local: false)
933 other_user = insert(:user, local: false)
934 activity = insert(:note_activity)
935 object = Object.normalize(activity)
937 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
940 |> put_req_header("accept", "application/activity+json")
941 |> assign(:user, user)
943 |> json_response(200)
946 |> put_req_header("accept", "application/activity+json")
947 |> assign(:user, other_user)
949 |> json_response(200)
951 assert Delivery.get(object.id, user.id)
952 assert Delivery.get(object.id, other_user.id)
955 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
956 user = insert(:user, local: false)
957 other_user = insert(:user, local: false)
958 activity = insert(:note_activity)
959 object = Object.normalize(activity)
961 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
964 |> put_req_header("accept", "application/activity+json")
965 |> assign(:user, user)
966 |> get(activity_path)
967 |> json_response(200)
970 |> put_req_header("accept", "application/activity+json")
971 |> assign(:user, other_user)
972 |> get(activity_path)
973 |> json_response(200)
975 assert Delivery.get(object.id, user.id)
976 assert Delivery.get(object.id, other_user.id)