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 "/activities/:uuid" do
229 test "it returns a json representation of the activity", %{conn: conn} do
230 activity = insert(:note_activity)
231 uuid = String.split(activity.data["id"], "/") |> List.last()
235 |> put_req_header("accept", "application/activity+json")
236 |> get("/activities/#{uuid}")
238 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
241 test "it returns 404 for non-public activities", %{conn: conn} do
242 activity = insert(:direct_note_activity)
243 uuid = String.split(activity.data["id"], "/") |> List.last()
247 |> put_req_header("accept", "application/activity+json")
248 |> get("/activities/#{uuid}")
250 assert json_response(conn, 404)
253 test "it caches a response", %{conn: conn} do
254 activity = insert(:note_activity)
255 uuid = String.split(activity.data["id"], "/") |> List.last()
259 |> put_req_header("accept", "application/activity+json")
260 |> get("/activities/#{uuid}")
262 assert json_response(conn1, :ok)
263 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
267 |> put_req_header("accept", "application/activity+json")
268 |> get("/activities/#{uuid}")
270 assert json_response(conn1, :ok) == json_response(conn2, :ok)
271 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
274 test "cached purged after activity deletion", %{conn: conn} do
276 {:ok, activity} = CommonAPI.post(user, %{"status" => "cofe"})
278 uuid = String.split(activity.data["id"], "/") |> List.last()
282 |> put_req_header("accept", "application/activity+json")
283 |> get("/activities/#{uuid}")
285 assert json_response(conn1, :ok)
286 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
288 Activity.delete_by_ap_id(activity.object.data["id"])
292 |> put_req_header("accept", "application/activity+json")
293 |> get("/activities/#{uuid}")
295 assert "Not found" == json_response(conn2, :not_found)
300 test "it inserts an incoming activity into the database", %{conn: conn} do
301 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
305 |> assign(:valid_signature, true)
306 |> put_req_header("content-type", "application/activity+json")
307 |> post("/inbox", data)
309 assert "ok" == json_response(conn, 200)
311 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
312 assert Activity.get_by_ap_id(data["id"])
315 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
316 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
318 sender_url = data["actor"]
319 Instances.set_consistently_unreachable(sender_url)
320 refute Instances.reachable?(sender_url)
324 |> assign(:valid_signature, true)
325 |> put_req_header("content-type", "application/activity+json")
326 |> post("/inbox", data)
328 assert "ok" == json_response(conn, 200)
329 assert Instances.reachable?(sender_url)
333 describe "/users/:nickname/inbox" do
336 File.read!("test/fixtures/mastodon-post-activity.json")
342 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
344 data = Map.put(data, "bcc", [user.ap_id])
348 |> assign(:valid_signature, true)
349 |> put_req_header("content-type", "application/activity+json")
350 |> post("/users/#{user.nickname}/inbox", data)
352 assert "ok" == json_response(conn, 200)
353 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
354 assert Activity.get_by_ap_id(data["id"])
357 test "it accepts messages from actors that are followed by the user", %{
361 recipient = insert(:user)
362 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
364 {:ok, recipient} = User.follow(recipient, actor)
368 |> Map.put("attributedTo", actor.ap_id)
372 |> Map.put("actor", actor.ap_id)
373 |> Map.put("object", object)
377 |> assign(:valid_signature, true)
378 |> put_req_header("content-type", "application/activity+json")
379 |> post("/users/#{recipient.nickname}/inbox", data)
381 assert "ok" == json_response(conn, 200)
382 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
383 assert Activity.get_by_ap_id(data["id"])
386 test "it rejects reads from other users", %{conn: conn} do
388 otheruser = insert(:user)
392 |> assign(:user, otheruser)
393 |> put_req_header("accept", "application/activity+json")
394 |> get("/users/#{user.nickname}/inbox")
396 assert json_response(conn, 403)
399 test "it doesn't crash without an authenticated user", %{conn: conn} do
404 |> put_req_header("accept", "application/activity+json")
405 |> get("/users/#{user.nickname}/inbox")
407 assert json_response(conn, 403)
410 test "it returns a note activity in a collection", %{conn: conn} do
411 note_activity = insert(:direct_note_activity)
412 note_object = Object.normalize(note_activity)
413 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
417 |> assign(:user, user)
418 |> put_req_header("accept", "application/activity+json")
419 |> get("/users/#{user.nickname}/inbox?page=true")
421 assert response(conn, 200) =~ note_object.data["content"]
424 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
426 data = Map.put(data, "bcc", [user.ap_id])
428 sender_host = URI.parse(data["actor"]).host
429 Instances.set_consistently_unreachable(sender_host)
430 refute Instances.reachable?(sender_host)
434 |> assign(:valid_signature, true)
435 |> put_req_header("content-type", "application/activity+json")
436 |> post("/users/#{user.nickname}/inbox", data)
438 assert "ok" == json_response(conn, 200)
439 assert Instances.reachable?(sender_host)
442 test "it removes all follower collections but actor's", %{conn: conn} do
443 [actor, recipient] = insert_pair(:user)
446 File.read!("test/fixtures/activitypub-client-post-activity.json")
449 object = Map.put(data["object"], "attributedTo", actor.ap_id)
453 |> Map.put("id", Utils.generate_object_id())
454 |> Map.put("actor", actor.ap_id)
455 |> Map.put("object", object)
457 recipient.follower_address,
458 actor.follower_address
462 recipient.follower_address,
463 "https://www.w3.org/ns/activitystreams#Public"
467 |> assign(:valid_signature, true)
468 |> put_req_header("content-type", "application/activity+json")
469 |> post("/users/#{recipient.nickname}/inbox", data)
470 |> json_response(200)
472 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
474 activity = Activity.get_by_ap_id(data["id"])
477 assert actor.follower_address in activity.recipients
478 assert actor.follower_address in activity.data["cc"]
480 refute recipient.follower_address in activity.recipients
481 refute recipient.follower_address in activity.data["cc"]
482 refute recipient.follower_address in activity.data["to"]
486 describe "/users/:nickname/outbox" do
487 test "it will not bomb when there is no activity", %{conn: conn} do
492 |> put_req_header("accept", "application/activity+json")
493 |> get("/users/#{user.nickname}/outbox")
495 result = json_response(conn, 200)
496 assert user.ap_id <> "/outbox" == result["id"]
499 test "it returns a note activity in a collection", %{conn: conn} do
500 note_activity = insert(:note_activity)
501 note_object = Object.normalize(note_activity)
502 user = User.get_cached_by_ap_id(note_activity.data["actor"])
506 |> put_req_header("accept", "application/activity+json")
507 |> get("/users/#{user.nickname}/outbox?page=true")
509 assert response(conn, 200) =~ note_object.data["content"]
512 test "it returns an announce activity in a collection", %{conn: conn} do
513 announce_activity = insert(:announce_activity)
514 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
518 |> put_req_header("accept", "application/activity+json")
519 |> get("/users/#{user.nickname}/outbox?page=true")
521 assert response(conn, 200) =~ announce_activity.data["object"]
524 test "it rejects posts from other users", %{conn: conn} do
525 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
527 otheruser = insert(:user)
531 |> assign(:user, otheruser)
532 |> put_req_header("content-type", "application/activity+json")
533 |> post("/users/#{user.nickname}/outbox", data)
535 assert json_response(conn, 403)
538 test "it inserts an incoming create activity into the database", %{conn: conn} do
539 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
544 |> assign(:user, user)
545 |> put_req_header("content-type", "application/activity+json")
546 |> post("/users/#{user.nickname}/outbox", data)
548 result = json_response(conn, 201)
550 assert Activity.get_by_ap_id(result["id"])
553 test "it rejects an incoming activity with bogus type", %{conn: conn} do
554 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
559 |> Map.put("type", "BadType")
563 |> assign(:user, user)
564 |> put_req_header("content-type", "application/activity+json")
565 |> post("/users/#{user.nickname}/outbox", data)
567 assert json_response(conn, 400)
570 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
571 note_activity = insert(:note_activity)
572 note_object = Object.normalize(note_activity)
573 user = User.get_cached_by_ap_id(note_activity.data["actor"])
578 id: note_object.data["id"]
584 |> assign(:user, user)
585 |> put_req_header("content-type", "application/activity+json")
586 |> post("/users/#{user.nickname}/outbox", data)
588 result = json_response(conn, 201)
589 assert Activity.get_by_ap_id(result["id"])
591 assert object = Object.get_by_ap_id(note_object.data["id"])
592 assert object.data["type"] == "Tombstone"
595 test "it rejects delete activity of object from other actor", %{conn: conn} do
596 note_activity = insert(:note_activity)
597 note_object = Object.normalize(note_activity)
603 id: note_object.data["id"]
609 |> assign(:user, user)
610 |> put_req_header("content-type", "application/activity+json")
611 |> post("/users/#{user.nickname}/outbox", data)
613 assert json_response(conn, 400)
616 test "it increases like count when receiving a like action", %{conn: conn} do
617 note_activity = insert(:note_activity)
618 note_object = Object.normalize(note_activity)
619 user = User.get_cached_by_ap_id(note_activity.data["actor"])
624 id: note_object.data["id"]
630 |> assign(:user, user)
631 |> put_req_header("content-type", "application/activity+json")
632 |> post("/users/#{user.nickname}/outbox", data)
634 result = json_response(conn, 201)
635 assert Activity.get_by_ap_id(result["id"])
637 assert object = Object.get_by_ap_id(note_object.data["id"])
638 assert object.data["like_count"] == 1
642 describe "/relay/followers" do
643 test "it returns relay followers", %{conn: conn} do
644 relay_actor = Relay.get_actor()
646 User.follow(user, relay_actor)
650 |> assign(:relay, true)
651 |> get("/relay/followers")
652 |> json_response(200)
654 assert result["first"]["orderedItems"] == [user.ap_id]
658 describe "/relay/following" do
659 test "it returns relay following", %{conn: conn} do
662 |> assign(:relay, true)
663 |> get("/relay/following")
664 |> json_response(200)
666 assert result["first"]["orderedItems"] == []
670 describe "/users/:nickname/followers" do
671 test "it returns the followers in a collection", %{conn: conn} do
673 user_two = insert(:user)
674 User.follow(user, user_two)
678 |> get("/users/#{user_two.nickname}/followers")
679 |> json_response(200)
681 assert result["first"]["orderedItems"] == [user.ap_id]
684 test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
686 user_two = insert(:user, %{info: %{hide_followers: true}})
687 User.follow(user, user_two)
691 |> get("/users/#{user_two.nickname}/followers")
692 |> json_response(200)
694 assert is_binary(result["first"])
697 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
699 user = insert(:user, %{info: %{hide_followers: true}})
703 |> get("/users/#{user.nickname}/followers?page=1")
705 assert result.status == 403
706 assert result.resp_body == ""
709 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
711 user = insert(:user, %{info: %{hide_followers: true}})
712 other_user = insert(:user)
713 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
717 |> assign(:user, user)
718 |> get("/users/#{user.nickname}/followers?page=1")
719 |> json_response(200)
721 assert result["totalItems"] == 1
722 assert result["orderedItems"] == [other_user.ap_id]
725 test "it works for more than 10 users", %{conn: conn} do
728 Enum.each(1..15, fn _ ->
729 other_user = insert(:user)
730 User.follow(other_user, user)
735 |> get("/users/#{user.nickname}/followers")
736 |> json_response(200)
738 assert length(result["first"]["orderedItems"]) == 10
739 assert result["first"]["totalItems"] == 15
740 assert result["totalItems"] == 15
744 |> get("/users/#{user.nickname}/followers?page=2")
745 |> json_response(200)
747 assert length(result["orderedItems"]) == 5
748 assert result["totalItems"] == 15
752 describe "/users/:nickname/following" do
753 test "it returns the following in a collection", %{conn: conn} do
755 user_two = insert(:user)
756 User.follow(user, user_two)
760 |> get("/users/#{user.nickname}/following")
761 |> json_response(200)
763 assert result["first"]["orderedItems"] == [user_two.ap_id]
766 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
767 user = insert(:user, %{info: %{hide_follows: true}})
768 user_two = insert(:user)
769 User.follow(user, user_two)
773 |> get("/users/#{user.nickname}/following")
774 |> json_response(200)
776 assert is_binary(result["first"])
779 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
781 user = insert(:user, %{info: %{hide_follows: true}})
785 |> get("/users/#{user.nickname}/following?page=1")
787 assert result.status == 403
788 assert result.resp_body == ""
791 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
793 user = insert(:user, %{info: %{hide_follows: true}})
794 other_user = insert(:user)
795 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
799 |> assign(:user, user)
800 |> get("/users/#{user.nickname}/following?page=1")
801 |> json_response(200)
803 assert result["totalItems"] == 1
804 assert result["orderedItems"] == [other_user.ap_id]
807 test "it works for more than 10 users", %{conn: conn} do
810 Enum.each(1..15, fn _ ->
811 user = User.get_cached_by_id(user.id)
812 other_user = insert(:user)
813 User.follow(user, other_user)
818 |> get("/users/#{user.nickname}/following")
819 |> json_response(200)
821 assert length(result["first"]["orderedItems"]) == 10
822 assert result["first"]["totalItems"] == 15
823 assert result["totalItems"] == 15
827 |> get("/users/#{user.nickname}/following?page=2")
828 |> json_response(200)
830 assert length(result["orderedItems"]) == 5
831 assert result["totalItems"] == 15
835 describe "delivery tracking" do
836 test "it tracks a signed object fetch", %{conn: conn} do
837 user = insert(:user, local: false)
838 activity = insert(:note_activity)
839 object = Object.normalize(activity)
841 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
844 |> put_req_header("accept", "application/activity+json")
845 |> assign(:user, user)
847 |> json_response(200)
849 assert Delivery.get(object.id, user.id)
852 test "it tracks a signed activity fetch", %{conn: conn} do
853 user = insert(:user, local: false)
854 activity = insert(:note_activity)
855 object = Object.normalize(activity)
857 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
860 |> put_req_header("accept", "application/activity+json")
861 |> assign(:user, user)
862 |> get(activity_path)
863 |> json_response(200)
865 assert Delivery.get(object.id, user.id)
868 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
869 user = insert(:user, local: false)
870 other_user = insert(:user, local: false)
871 activity = insert(:note_activity)
872 object = Object.normalize(activity)
874 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
877 |> put_req_header("accept", "application/activity+json")
878 |> assign(:user, user)
880 |> json_response(200)
883 |> put_req_header("accept", "application/activity+json")
884 |> assign(:user, other_user)
886 |> json_response(200)
888 assert Delivery.get(object.id, user.id)
889 assert Delivery.get(object.id, other_user.id)
892 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
893 user = insert(:user, local: false)
894 other_user = insert(:user, local: false)
895 activity = insert(:note_activity)
896 object = Object.normalize(activity)
898 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
901 |> put_req_header("accept", "application/activity+json")
902 |> assign(:user, user)
903 |> get(activity_path)
904 |> json_response(200)
907 |> put_req_header("accept", "application/activity+json")
908 |> assign(:user, other_user)
909 |> get(activity_path)
910 |> json_response(200)
912 assert Delivery.get(object.id, user.id)
913 assert Delivery.get(object.id, other_user.id)
917 describe "Additionnal ActivityPub C2S endpoints" do
918 test "/api/ap/whoami", %{conn: conn} do
923 |> assign(:user, user)
924 |> get("/api/ap/whoami")
926 user = User.get_cached_by_id(user.id)
928 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
931 clear_config([:media_proxy])
932 clear_config([Pleroma.Upload])
934 test "uploadMedia", %{conn: conn} do
937 desc = "Description of the image"
939 image = %Plug.Upload{
940 content_type: "image/jpg",
941 path: Path.absname("test/fixtures/image.jpg"),
942 filename: "an_image.jpg"
947 |> assign(:user, user)
948 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
950 assert object = json_response(conn, :created)
951 assert object["name"] == desc
952 assert object["type"] == "Document"
953 assert object["actor"] == user.ap_id