1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 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
9 alias Pleroma.Instances
12 alias Pleroma.Web.ActivityPub.ObjectView
13 alias Pleroma.Web.ActivityPub.Relay
14 alias Pleroma.Web.ActivityPub.UserView
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.CommonAPI
19 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
23 clear_config_all([:instance, :federating],
24 do: Pleroma.Config.put([:instance, :federating], true)
28 clear_config([:instance, :allow_relay])
30 test "with the relay active, it returns the relay user", %{conn: conn} do
33 |> get(activity_pub_path(conn, :relay))
36 assert res["id"] =~ "/relay"
39 test "with the relay disabled, it returns 404", %{conn: conn} do
40 Pleroma.Config.put([:instance, :allow_relay], false)
43 |> get(activity_pub_path(conn, :relay))
49 describe "/internal/fetch" do
50 test "it returns the internal fetch user", %{conn: conn} do
53 |> get(activity_pub_path(conn, :internal_fetch))
56 assert res["id"] =~ "/fetch"
60 describe "/users/:nickname" do
61 test "it returns a json representation of the user with accept application/json", %{
68 |> put_req_header("accept", "application/json")
69 |> get("/users/#{user.nickname}")
71 user = User.get_cached_by_id(user.id)
73 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
76 test "it returns a json representation of the user with accept application/activity+json", %{
83 |> put_req_header("accept", "application/activity+json")
84 |> get("/users/#{user.nickname}")
86 user = User.get_cached_by_id(user.id)
88 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
91 test "it returns a json representation of the user with accept application/ld+json", %{
100 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
102 |> get("/users/#{user.nickname}")
104 user = User.get_cached_by_id(user.id)
106 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
110 describe "/object/:uuid" do
111 test "it returns a json representation of the object with accept application/json", %{
115 uuid = String.split(note.data["id"], "/") |> List.last()
119 |> put_req_header("accept", "application/json")
120 |> get("/objects/#{uuid}")
122 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
125 test "it returns a json representation of the object with accept application/activity+json",
128 uuid = String.split(note.data["id"], "/") |> List.last()
132 |> put_req_header("accept", "application/activity+json")
133 |> get("/objects/#{uuid}")
135 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
138 test "it returns a json representation of the object with accept application/ld+json", %{
142 uuid = String.split(note.data["id"], "/") |> List.last()
148 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
150 |> get("/objects/#{uuid}")
152 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
155 test "it returns 404 for non-public messages", %{conn: conn} do
156 note = insert(:direct_note)
157 uuid = String.split(note.data["id"], "/") |> List.last()
161 |> put_req_header("accept", "application/activity+json")
162 |> get("/objects/#{uuid}")
164 assert json_response(conn, 404)
167 test "it returns 404 for tombstone objects", %{conn: conn} do
168 tombstone = insert(:tombstone)
169 uuid = String.split(tombstone.data["id"], "/") |> List.last()
173 |> put_req_header("accept", "application/activity+json")
174 |> get("/objects/#{uuid}")
176 assert json_response(conn, 404)
179 test "it caches a response", %{conn: conn} do
181 uuid = String.split(note.data["id"], "/") |> List.last()
185 |> put_req_header("accept", "application/activity+json")
186 |> get("/objects/#{uuid}")
188 assert json_response(conn1, :ok)
189 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
193 |> put_req_header("accept", "application/activity+json")
194 |> get("/objects/#{uuid}")
196 assert json_response(conn1, :ok) == json_response(conn2, :ok)
197 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
200 test "cached purged after object deletion", %{conn: conn} do
202 uuid = String.split(note.data["id"], "/") |> List.last()
206 |> put_req_header("accept", "application/activity+json")
207 |> get("/objects/#{uuid}")
209 assert json_response(conn1, :ok)
210 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
216 |> put_req_header("accept", "application/activity+json")
217 |> get("/objects/#{uuid}")
219 assert "Not found" == json_response(conn2, :not_found)
223 describe "/object/:uuid/likes" do
225 like = insert(:like_activity)
226 like_object_ap_id = Object.normalize(like).data["id"]
233 [id: like.data["id"], uuid: uuid]
236 test "it returns the like activities in a collection", %{conn: conn, id: id, uuid: uuid} do
239 |> put_req_header("accept", "application/activity+json")
240 |> get("/objects/#{uuid}/likes")
241 |> json_response(200)
243 assert List.first(result["first"]["orderedItems"])["id"] == id
244 assert result["type"] == "OrderedCollection"
245 assert result["totalItems"] == 1
246 refute result["first"]["next"]
249 test "it does not crash when page number is exceeded total pages", %{conn: conn, uuid: uuid} do
252 |> put_req_header("accept", "application/activity+json")
253 |> get("/objects/#{uuid}/likes?page=2")
254 |> json_response(200)
256 assert result["type"] == "OrderedCollectionPage"
257 assert result["totalItems"] == 1
258 refute result["next"]
259 assert Enum.empty?(result["orderedItems"])
262 test "it contains the next key when likes count is more than 10", %{conn: conn} do
263 note = insert(:note_activity)
264 insert_list(11, :like_activity, note_activity: note)
268 |> Object.normalize()
276 |> put_req_header("accept", "application/activity+json")
277 |> get("/objects/#{uuid}/likes?page=1")
278 |> json_response(200)
280 assert result["totalItems"] == 11
281 assert length(result["orderedItems"]) == 10
282 assert result["next"]
286 describe "/activities/:uuid" do
287 test "it returns a json representation of the activity", %{conn: conn} do
288 activity = insert(:note_activity)
289 uuid = String.split(activity.data["id"], "/") |> List.last()
293 |> put_req_header("accept", "application/activity+json")
294 |> get("/activities/#{uuid}")
296 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
299 test "it returns 404 for non-public activities", %{conn: conn} do
300 activity = insert(:direct_note_activity)
301 uuid = String.split(activity.data["id"], "/") |> List.last()
305 |> put_req_header("accept", "application/activity+json")
306 |> get("/activities/#{uuid}")
308 assert json_response(conn, 404)
311 test "it caches a response", %{conn: conn} do
312 activity = insert(:note_activity)
313 uuid = String.split(activity.data["id"], "/") |> List.last()
317 |> put_req_header("accept", "application/activity+json")
318 |> get("/activities/#{uuid}")
320 assert json_response(conn1, :ok)
321 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
325 |> put_req_header("accept", "application/activity+json")
326 |> get("/activities/#{uuid}")
328 assert json_response(conn1, :ok) == json_response(conn2, :ok)
329 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
332 test "cached purged after activity deletion", %{conn: conn} do
334 {:ok, activity} = CommonAPI.post(user, %{"status" => "cofe"})
336 uuid = String.split(activity.data["id"], "/") |> List.last()
340 |> put_req_header("accept", "application/activity+json")
341 |> get("/activities/#{uuid}")
343 assert json_response(conn1, :ok)
344 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
346 Activity.delete_by_ap_id(activity.object.data["id"])
350 |> put_req_header("accept", "application/activity+json")
351 |> get("/activities/#{uuid}")
353 assert "Not found" == json_response(conn2, :not_found)
358 test "it inserts an incoming activity into the database", %{conn: conn} do
359 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
363 |> assign(:valid_signature, true)
364 |> put_req_header("content-type", "application/activity+json")
365 |> post("/inbox", data)
367 assert "ok" == json_response(conn, 200)
369 assert Activity.get_by_ap_id(data["id"])
372 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
373 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
375 sender_url = data["actor"]
376 Instances.set_consistently_unreachable(sender_url)
377 refute Instances.reachable?(sender_url)
381 |> assign(:valid_signature, true)
382 |> put_req_header("content-type", "application/activity+json")
383 |> post("/inbox", data)
385 assert "ok" == json_response(conn, 200)
386 assert Instances.reachable?(sender_url)
390 describe "/users/:nickname/inbox" do
393 File.read!("test/fixtures/mastodon-post-activity.json")
399 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
401 data = Map.put(data, "bcc", [user.ap_id])
405 |> assign(:valid_signature, true)
406 |> put_req_header("content-type", "application/activity+json")
407 |> post("/users/#{user.nickname}/inbox", data)
409 assert "ok" == json_response(conn, 200)
411 assert Activity.get_by_ap_id(data["id"])
414 test "it accepts messages from actors that are followed by the user", %{
418 recipient = insert(:user)
419 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
421 {:ok, recipient} = User.follow(recipient, actor)
425 |> Map.put("attributedTo", actor.ap_id)
429 |> Map.put("actor", actor.ap_id)
430 |> Map.put("object", object)
434 |> assign(:valid_signature, true)
435 |> put_req_header("content-type", "application/activity+json")
436 |> post("/users/#{recipient.nickname}/inbox", data)
438 assert "ok" == json_response(conn, 200)
440 assert Activity.get_by_ap_id(data["id"])
443 test "it rejects reads from other users", %{conn: conn} do
445 otheruser = insert(:user)
449 |> assign(:user, otheruser)
450 |> put_req_header("accept", "application/activity+json")
451 |> get("/users/#{user.nickname}/inbox")
453 assert json_response(conn, 403)
456 test "it doesn't crash without an authenticated user", %{conn: conn} do
461 |> put_req_header("accept", "application/activity+json")
462 |> get("/users/#{user.nickname}/inbox")
464 assert json_response(conn, 403)
467 test "it returns a note activity in a collection", %{conn: conn} do
468 note_activity = insert(:direct_note_activity)
469 note_object = Object.normalize(note_activity)
470 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
474 |> assign(:user, user)
475 |> put_req_header("accept", "application/activity+json")
476 |> get("/users/#{user.nickname}/inbox?page=true")
478 assert response(conn, 200) =~ note_object.data["content"]
481 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
483 data = Map.put(data, "bcc", [user.ap_id])
485 sender_host = URI.parse(data["actor"]).host
486 Instances.set_consistently_unreachable(sender_host)
487 refute Instances.reachable?(sender_host)
491 |> assign(:valid_signature, true)
492 |> put_req_header("content-type", "application/activity+json")
493 |> post("/users/#{user.nickname}/inbox", data)
495 assert "ok" == json_response(conn, 200)
496 assert Instances.reachable?(sender_host)
499 test "it removes all follower collections but actor's", %{conn: conn} do
500 [actor, recipient] = insert_pair(:user)
503 File.read!("test/fixtures/activitypub-client-post-activity.json")
506 object = Map.put(data["object"], "attributedTo", actor.ap_id)
510 |> Map.put("id", Utils.generate_object_id())
511 |> Map.put("actor", actor.ap_id)
512 |> Map.put("object", object)
514 recipient.follower_address,
515 actor.follower_address
519 recipient.follower_address,
520 "https://www.w3.org/ns/activitystreams#Public"
524 |> assign(:valid_signature, true)
525 |> put_req_header("content-type", "application/activity+json")
526 |> post("/users/#{recipient.nickname}/inbox", data)
527 |> json_response(200)
529 activity = Activity.get_by_ap_id(data["id"])
532 assert actor.follower_address in activity.recipients
533 assert actor.follower_address in activity.data["cc"]
535 refute recipient.follower_address in activity.recipients
536 refute recipient.follower_address in activity.data["cc"]
537 refute recipient.follower_address in activity.data["to"]
541 describe "/users/:nickname/outbox" do
542 test "it will not bomb when there is no activity", %{conn: conn} do
547 |> put_req_header("accept", "application/activity+json")
548 |> get("/users/#{user.nickname}/outbox")
550 result = json_response(conn, 200)
551 assert user.ap_id <> "/outbox" == result["id"]
554 test "it returns a note activity in a collection", %{conn: conn} do
555 note_activity = insert(:note_activity)
556 note_object = Object.normalize(note_activity)
557 user = User.get_cached_by_ap_id(note_activity.data["actor"])
561 |> put_req_header("accept", "application/activity+json")
562 |> get("/users/#{user.nickname}/outbox?page=true")
564 assert response(conn, 200) =~ note_object.data["content"]
567 test "it returns an announce activity in a collection", %{conn: conn} do
568 announce_activity = insert(:announce_activity)
569 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
573 |> put_req_header("accept", "application/activity+json")
574 |> get("/users/#{user.nickname}/outbox?page=true")
576 assert response(conn, 200) =~ announce_activity.data["object"]
579 test "it rejects posts from other users", %{conn: conn} do
580 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
582 otheruser = insert(:user)
586 |> assign(:user, otheruser)
587 |> put_req_header("content-type", "application/activity+json")
588 |> post("/users/#{user.nickname}/outbox", data)
590 assert json_response(conn, 403)
593 test "it inserts an incoming create activity into the database", %{conn: conn} do
594 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
599 |> assign(:user, user)
600 |> put_req_header("content-type", "application/activity+json")
601 |> post("/users/#{user.nickname}/outbox", data)
603 result = json_response(conn, 201)
604 assert Activity.get_by_ap_id(result["id"])
607 test "it rejects an incoming activity with bogus type", %{conn: conn} do
608 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
613 |> Map.put("type", "BadType")
617 |> assign(:user, user)
618 |> put_req_header("content-type", "application/activity+json")
619 |> post("/users/#{user.nickname}/outbox", data)
621 assert json_response(conn, 400)
624 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
625 note_activity = insert(:note_activity)
626 note_object = Object.normalize(note_activity)
627 user = User.get_cached_by_ap_id(note_activity.data["actor"])
632 id: note_object.data["id"]
638 |> assign(:user, user)
639 |> put_req_header("content-type", "application/activity+json")
640 |> post("/users/#{user.nickname}/outbox", data)
642 result = json_response(conn, 201)
643 assert Activity.get_by_ap_id(result["id"])
645 assert object = Object.get_by_ap_id(note_object.data["id"])
646 assert object.data["type"] == "Tombstone"
649 test "it rejects delete activity of object from other actor", %{conn: conn} do
650 note_activity = insert(:note_activity)
651 note_object = Object.normalize(note_activity)
657 id: note_object.data["id"]
663 |> assign(:user, user)
664 |> put_req_header("content-type", "application/activity+json")
665 |> post("/users/#{user.nickname}/outbox", data)
667 assert json_response(conn, 400)
670 test "it increases like count when receiving a like action", %{conn: conn} do
671 note_activity = insert(:note_activity)
672 note_object = Object.normalize(note_activity)
673 user = User.get_cached_by_ap_id(note_activity.data["actor"])
678 id: note_object.data["id"]
684 |> assign(:user, user)
685 |> put_req_header("content-type", "application/activity+json")
686 |> post("/users/#{user.nickname}/outbox", data)
688 result = json_response(conn, 201)
689 assert Activity.get_by_ap_id(result["id"])
691 assert object = Object.get_by_ap_id(note_object.data["id"])
692 assert object.data["like_count"] == 1
696 describe "/relay/followers" do
697 test "it returns relay followers", %{conn: conn} do
698 relay_actor = Relay.get_actor()
700 User.follow(user, relay_actor)
704 |> assign(:relay, true)
705 |> get("/relay/followers")
706 |> json_response(200)
708 assert result["first"]["orderedItems"] == [user.ap_id]
712 describe "/relay/following" do
713 test "it returns relay following", %{conn: conn} do
716 |> assign(:relay, true)
717 |> get("/relay/following")
718 |> json_response(200)
720 assert result["first"]["orderedItems"] == []
724 describe "/users/:nickname/followers" do
725 test "it returns the followers in a collection", %{conn: conn} do
727 user_two = insert(:user)
728 User.follow(user, user_two)
732 |> get("/users/#{user_two.nickname}/followers")
733 |> json_response(200)
735 assert result["first"]["orderedItems"] == [user.ap_id]
738 test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
740 user_two = insert(:user, %{info: %{hide_followers: true}})
741 User.follow(user, user_two)
745 |> get("/users/#{user_two.nickname}/followers")
746 |> json_response(200)
748 assert is_binary(result["first"])
751 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
753 user = insert(:user, %{info: %{hide_followers: true}})
757 |> get("/users/#{user.nickname}/followers?page=1")
759 assert result.status == 403
760 assert result.resp_body == ""
763 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
765 user = insert(:user, %{info: %{hide_followers: true}})
766 other_user = insert(:user)
767 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
771 |> assign(:user, user)
772 |> get("/users/#{user.nickname}/followers?page=1")
773 |> json_response(200)
775 assert result["totalItems"] == 1
776 assert result["orderedItems"] == [other_user.ap_id]
779 test "it works for more than 10 users", %{conn: conn} do
782 Enum.each(1..15, fn _ ->
783 other_user = insert(:user)
784 User.follow(other_user, user)
789 |> get("/users/#{user.nickname}/followers")
790 |> json_response(200)
792 assert length(result["first"]["orderedItems"]) == 10
793 assert result["first"]["totalItems"] == 15
794 assert result["totalItems"] == 15
798 |> get("/users/#{user.nickname}/followers?page=2")
799 |> json_response(200)
801 assert length(result["orderedItems"]) == 5
802 assert result["totalItems"] == 15
806 describe "/users/:nickname/following" do
807 test "it returns the following in a collection", %{conn: conn} do
809 user_two = insert(:user)
810 User.follow(user, user_two)
814 |> get("/users/#{user.nickname}/following")
815 |> json_response(200)
817 assert result["first"]["orderedItems"] == [user_two.ap_id]
820 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
821 user = insert(:user, %{info: %{hide_follows: true}})
822 user_two = insert(:user)
823 User.follow(user, user_two)
827 |> get("/users/#{user.nickname}/following")
828 |> json_response(200)
830 assert is_binary(result["first"])
833 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
835 user = insert(:user, %{info: %{hide_follows: true}})
839 |> get("/users/#{user.nickname}/following?page=1")
841 assert result.status == 403
842 assert result.resp_body == ""
845 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
847 user = insert(:user, %{info: %{hide_follows: true}})
848 other_user = insert(:user)
849 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
853 |> assign(:user, user)
854 |> get("/users/#{user.nickname}/following?page=1")
855 |> json_response(200)
857 assert result["totalItems"] == 1
858 assert result["orderedItems"] == [other_user.ap_id]
861 test "it works for more than 10 users", %{conn: conn} do
864 Enum.each(1..15, fn _ ->
865 user = User.get_cached_by_id(user.id)
866 other_user = insert(:user)
867 User.follow(user, other_user)
872 |> get("/users/#{user.nickname}/following")
873 |> json_response(200)
875 assert length(result["first"]["orderedItems"]) == 10
876 assert result["first"]["totalItems"] == 15
877 assert result["totalItems"] == 15
881 |> get("/users/#{user.nickname}/following?page=2")
882 |> json_response(200)
884 assert length(result["orderedItems"]) == 5
885 assert result["totalItems"] == 15