1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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
11 alias Pleroma.Delivery
12 alias Pleroma.Instances
14 alias Pleroma.Tests.ObanHelpers
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.ObjectView
18 alias Pleroma.Web.ActivityPub.Relay
19 alias Pleroma.Web.ActivityPub.UserView
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Workers.ReceiverWorker
25 import Pleroma.Factory
27 require Pleroma.Constants
30 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
34 setup do: clear_config([:instance, :federating], true)
37 setup do: clear_config([:instance, :allow_relay])
39 test "with the relay active, it returns the relay user", %{conn: conn} do
42 |> get(activity_pub_path(conn, :relay))
45 assert res["id"] =~ "/relay"
48 test "with the relay disabled, it returns 404", %{conn: conn} do
49 Config.put([:instance, :allow_relay], false)
52 |> get(activity_pub_path(conn, :relay))
56 test "on non-federating instance, it returns 404", %{conn: conn} do
57 Config.put([:instance, :federating], false)
61 |> assign(:user, user)
62 |> get(activity_pub_path(conn, :relay))
67 describe "/internal/fetch" do
68 test "it returns the internal fetch user", %{conn: conn} do
71 |> get(activity_pub_path(conn, :internal_fetch))
74 assert res["id"] =~ "/fetch"
77 test "on non-federating instance, it returns 404", %{conn: conn} do
78 Config.put([:instance, :federating], false)
82 |> assign(:user, user)
83 |> get(activity_pub_path(conn, :internal_fetch))
88 describe "/users/:nickname" do
89 test "it returns a json representation of the user with accept application/json", %{
96 |> put_req_header("accept", "application/json")
97 |> get("/users/#{user.nickname}")
99 user = User.get_cached_by_id(user.id)
101 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
104 test "it returns a json representation of the user with accept application/activity+json", %{
111 |> put_req_header("accept", "application/activity+json")
112 |> get("/users/#{user.nickname}")
114 user = User.get_cached_by_id(user.id)
116 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
119 test "it returns a json representation of the user with accept application/ld+json", %{
128 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
130 |> get("/users/#{user.nickname}")
132 user = User.get_cached_by_id(user.id)
134 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
137 test "it returns 404 for remote users", %{
140 user = insert(:user, local: false, nickname: "remoteuser@example.com")
144 |> put_req_header("accept", "application/json")
145 |> get("/users/#{user.nickname}.json")
147 assert json_response(conn, 404)
150 test "it returns error when user is not found", %{conn: conn} do
153 |> put_req_header("accept", "application/json")
154 |> get("/users/jimm")
155 |> json_response(404)
157 assert response == "Not found"
161 describe "mastodon compatibility routes" do
162 test "it returns a json representation of the object with accept application/json", %{
169 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
170 "actor" => Endpoint.url() <> "/users/raymoo",
171 "to" => [Pleroma.Constants.as_public()]
177 |> put_req_header("accept", "application/json")
178 |> get("/users/raymoo/statuses/999999999")
180 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
183 test "it returns a json representation of the activity with accept application/json", %{
190 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
191 "actor" => Endpoint.url() <> "/users/raymoo",
192 "to" => [Pleroma.Constants.as_public()]
198 "id" => object.data["id"] <> "/activity",
200 "object" => object.data["id"],
201 "actor" => object.data["actor"],
202 "to" => object.data["to"]
204 |> ActivityPub.persist(local: true)
208 |> put_req_header("accept", "application/json")
209 |> get("/users/raymoo/statuses/999999999/activity")
211 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
215 describe "/objects/:uuid" do
216 test "it doesn't return a local-only object", %{conn: conn} do
218 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
220 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
222 object = Object.normalize(post, false)
223 uuid = String.split(object.data["id"], "/") |> List.last()
227 |> put_req_header("accept", "application/json")
228 |> get("/objects/#{uuid}")
230 assert json_response(conn, 404)
233 test "it returns a json representation of the object with accept application/json", %{
237 uuid = String.split(note.data["id"], "/") |> List.last()
241 |> put_req_header("accept", "application/json")
242 |> get("/objects/#{uuid}")
244 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
247 test "it returns a json representation of the object with accept application/activity+json",
250 uuid = String.split(note.data["id"], "/") |> List.last()
254 |> put_req_header("accept", "application/activity+json")
255 |> get("/objects/#{uuid}")
257 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
260 test "it returns a json representation of the object with accept application/ld+json", %{
264 uuid = String.split(note.data["id"], "/") |> List.last()
270 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
272 |> get("/objects/#{uuid}")
274 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
277 test "it returns 404 for non-public messages", %{conn: conn} do
278 note = insert(:direct_note)
279 uuid = String.split(note.data["id"], "/") |> List.last()
283 |> put_req_header("accept", "application/activity+json")
284 |> get("/objects/#{uuid}")
286 assert json_response(conn, 404)
289 test "it returns 404 for tombstone objects", %{conn: conn} do
290 tombstone = insert(:tombstone)
291 uuid = String.split(tombstone.data["id"], "/") |> List.last()
295 |> put_req_header("accept", "application/activity+json")
296 |> get("/objects/#{uuid}")
298 assert json_response(conn, 404)
301 test "it caches a response", %{conn: conn} do
303 uuid = String.split(note.data["id"], "/") |> List.last()
307 |> put_req_header("accept", "application/activity+json")
308 |> get("/objects/#{uuid}")
310 assert json_response(conn1, :ok)
311 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
315 |> put_req_header("accept", "application/activity+json")
316 |> get("/objects/#{uuid}")
318 assert json_response(conn1, :ok) == json_response(conn2, :ok)
319 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
322 test "cached purged after object deletion", %{conn: conn} do
324 uuid = String.split(note.data["id"], "/") |> List.last()
328 |> put_req_header("accept", "application/activity+json")
329 |> get("/objects/#{uuid}")
331 assert json_response(conn1, :ok)
332 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
338 |> put_req_header("accept", "application/activity+json")
339 |> get("/objects/#{uuid}")
341 assert "Not found" == json_response(conn2, :not_found)
345 describe "/activities/:uuid" do
346 test "it doesn't return a local-only activity", %{conn: conn} do
348 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
350 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
352 uuid = String.split(post.data["id"], "/") |> List.last()
356 |> put_req_header("accept", "application/json")
357 |> get("/activities/#{uuid}")
359 assert json_response(conn, 404)
362 test "it returns a json representation of the activity", %{conn: conn} do
363 activity = insert(:note_activity)
364 uuid = String.split(activity.data["id"], "/") |> List.last()
368 |> put_req_header("accept", "application/activity+json")
369 |> get("/activities/#{uuid}")
371 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
374 test "it returns 404 for non-public activities", %{conn: conn} do
375 activity = insert(:direct_note_activity)
376 uuid = String.split(activity.data["id"], "/") |> List.last()
380 |> put_req_header("accept", "application/activity+json")
381 |> get("/activities/#{uuid}")
383 assert json_response(conn, 404)
386 test "it caches a response", %{conn: conn} do
387 activity = insert(:note_activity)
388 uuid = String.split(activity.data["id"], "/") |> List.last()
392 |> put_req_header("accept", "application/activity+json")
393 |> get("/activities/#{uuid}")
395 assert json_response(conn1, :ok)
396 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
400 |> put_req_header("accept", "application/activity+json")
401 |> get("/activities/#{uuid}")
403 assert json_response(conn1, :ok) == json_response(conn2, :ok)
404 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
407 test "cached purged after activity deletion", %{conn: conn} do
409 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
411 uuid = String.split(activity.data["id"], "/") |> List.last()
415 |> put_req_header("accept", "application/activity+json")
416 |> get("/activities/#{uuid}")
418 assert json_response(conn1, :ok)
419 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
421 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
425 |> put_req_header("accept", "application/activity+json")
426 |> get("/activities/#{uuid}")
428 assert "Not found" == json_response(conn2, :not_found)
433 test "it inserts an incoming activity into the database", %{conn: conn} do
434 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
438 |> assign(:valid_signature, true)
439 |> put_req_header("content-type", "application/activity+json")
440 |> post("/inbox", data)
442 assert "ok" == json_response(conn, 200)
444 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
445 assert Activity.get_by_ap_id(data["id"])
448 @tag capture_log: true
449 test "it inserts an incoming activity into the database" <>
450 "even if we can't fetch the user but have it in our db",
454 ap_id: "https://mastodon.example.org/users/raymoo",
457 last_refreshed_at: nil
461 File.read!("test/fixtures/mastodon-post-activity.json")
463 |> Map.put("actor", user.ap_id)
464 |> put_in(["object", "attridbutedTo"], user.ap_id)
468 |> assign(:valid_signature, true)
469 |> put_req_header("content-type", "application/activity+json")
470 |> post("/inbox", data)
472 assert "ok" == json_response(conn, 200)
474 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
475 assert Activity.get_by_ap_id(data["id"])
478 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
479 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
481 sender_url = data["actor"]
482 Instances.set_consistently_unreachable(sender_url)
483 refute Instances.reachable?(sender_url)
487 |> assign(:valid_signature, true)
488 |> put_req_header("content-type", "application/activity+json")
489 |> post("/inbox", data)
491 assert "ok" == json_response(conn, 200)
492 assert Instances.reachable?(sender_url)
495 test "accept follow activity", %{conn: conn} do
496 Pleroma.Config.put([:instance, :federating], true)
497 relay = Relay.get_actor()
499 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
501 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
502 relay = refresh_record(relay)
505 File.read!("test/fixtures/relay/accept-follow.json")
506 |> String.replace("{{ap_id}}", relay.ap_id)
507 |> String.replace("{{activity_id}}", activity.data["id"])
511 |> assign(:valid_signature, true)
512 |> put_req_header("content-type", "application/activity+json")
513 |> post("/inbox", accept)
514 |> json_response(200)
516 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
518 assert Pleroma.FollowingRelationship.following?(
523 Mix.shell(Mix.Shell.Process)
526 Mix.shell(Mix.Shell.IO)
529 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
530 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
533 @tag capture_log: true
534 test "without valid signature, " <>
535 "it only accepts Create activities and requires enabled federation",
537 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
538 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
540 conn = put_req_header(conn, "content-type", "application/activity+json")
542 Config.put([:instance, :federating], false)
545 |> post("/inbox", data)
546 |> json_response(403)
549 |> post("/inbox", non_create_data)
550 |> json_response(403)
552 Config.put([:instance, :federating], true)
554 ret_conn = post(conn, "/inbox", data)
555 assert "ok" == json_response(ret_conn, 200)
558 |> post("/inbox", non_create_data)
559 |> json_response(400)
563 describe "/users/:nickname/inbox" do
566 File.read!("test/fixtures/mastodon-post-activity.json")
572 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
574 data = Map.put(data, "bcc", [user.ap_id])
578 |> assign(:valid_signature, true)
579 |> put_req_header("content-type", "application/activity+json")
580 |> post("/users/#{user.nickname}/inbox", data)
582 assert "ok" == json_response(conn, 200)
583 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
584 assert Activity.get_by_ap_id(data["id"])
587 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
591 Map.put(data, "to", user.ap_id)
596 |> assign(:valid_signature, true)
597 |> put_req_header("content-type", "application/activity+json")
598 |> post("/users/#{user.nickname}/inbox", data)
600 assert "ok" == json_response(conn, 200)
601 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
602 assert Activity.get_by_ap_id(data["id"])
605 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
609 Map.put(data, "cc", user.ap_id)
614 |> assign(:valid_signature, true)
615 |> put_req_header("content-type", "application/activity+json")
616 |> post("/users/#{user.nickname}/inbox", data)
618 assert "ok" == json_response(conn, 200)
619 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
620 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
621 assert user.ap_id in activity.recipients
624 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
628 Map.put(data, "bcc", user.ap_id)
634 |> assign(:valid_signature, true)
635 |> put_req_header("content-type", "application/activity+json")
636 |> post("/users/#{user.nickname}/inbox", data)
638 assert "ok" == json_response(conn, 200)
639 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
640 assert Activity.get_by_ap_id(data["id"])
643 test "it accepts announces with to as string instead of array", %{conn: conn} do
646 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
647 announcer = insert(:user, local: false)
650 "@context" => "https://www.w3.org/ns/activitystreams",
651 "actor" => announcer.ap_id,
652 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
653 "object" => post.data["object"],
654 "to" => "https://www.w3.org/ns/activitystreams#Public",
655 "cc" => [user.ap_id],
661 |> assign(:valid_signature, true)
662 |> put_req_header("content-type", "application/activity+json")
663 |> post("/users/#{user.nickname}/inbox", data)
665 assert "ok" == json_response(conn, 200)
666 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
667 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
668 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
671 test "it accepts messages from actors that are followed by the user", %{
675 recipient = insert(:user)
676 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
678 {:ok, recipient} = User.follow(recipient, actor)
682 |> Map.put("attributedTo", actor.ap_id)
686 |> Map.put("actor", actor.ap_id)
687 |> Map.put("object", object)
691 |> assign(:valid_signature, true)
692 |> put_req_header("content-type", "application/activity+json")
693 |> post("/users/#{recipient.nickname}/inbox", data)
695 assert "ok" == json_response(conn, 200)
696 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
697 assert Activity.get_by_ap_id(data["id"])
700 test "it rejects reads from other users", %{conn: conn} do
702 other_user = insert(:user)
706 |> assign(:user, other_user)
707 |> put_req_header("accept", "application/activity+json")
708 |> get("/users/#{user.nickname}/inbox")
710 assert json_response(conn, 403)
713 test "it returns a note activity in a collection", %{conn: conn} do
714 note_activity = insert(:direct_note_activity)
715 note_object = Object.normalize(note_activity)
716 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
720 |> assign(:user, user)
721 |> put_req_header("accept", "application/activity+json")
722 |> get("/users/#{user.nickname}/inbox?page=true")
724 assert response(conn, 200) =~ note_object.data["content"]
727 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
729 data = Map.put(data, "bcc", [user.ap_id])
731 sender_host = URI.parse(data["actor"]).host
732 Instances.set_consistently_unreachable(sender_host)
733 refute Instances.reachable?(sender_host)
737 |> assign(:valid_signature, true)
738 |> put_req_header("content-type", "application/activity+json")
739 |> post("/users/#{user.nickname}/inbox", data)
741 assert "ok" == json_response(conn, 200)
742 assert Instances.reachable?(sender_host)
745 test "it removes all follower collections but actor's", %{conn: conn} do
746 [actor, recipient] = insert_pair(:user)
749 File.read!("test/fixtures/activitypub-client-post-activity.json")
752 object = Map.put(data["object"], "attributedTo", actor.ap_id)
756 |> Map.put("id", Utils.generate_object_id())
757 |> Map.put("actor", actor.ap_id)
758 |> Map.put("object", object)
760 recipient.follower_address,
761 actor.follower_address
765 recipient.follower_address,
766 "https://www.w3.org/ns/activitystreams#Public"
770 |> assign(:valid_signature, true)
771 |> put_req_header("content-type", "application/activity+json")
772 |> post("/users/#{recipient.nickname}/inbox", data)
773 |> json_response(200)
775 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
777 activity = Activity.get_by_ap_id(data["id"])
780 assert actor.follower_address in activity.recipients
781 assert actor.follower_address in activity.data["cc"]
783 refute recipient.follower_address in activity.recipients
784 refute recipient.follower_address in activity.data["cc"]
785 refute recipient.follower_address in activity.data["to"]
788 test "it requires authentication", %{conn: conn} do
790 conn = put_req_header(conn, "accept", "application/activity+json")
792 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
793 assert json_response(ret_conn, 403)
797 |> assign(:user, user)
798 |> get("/users/#{user.nickname}/inbox")
800 assert json_response(ret_conn, 200)
804 describe "GET /users/:nickname/outbox" do
805 test "it paginates correctly", %{conn: conn} do
807 conn = assign(conn, :user, user)
808 outbox_endpoint = user.ap_id <> "/outbox"
812 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
818 |> put_req_header("accept", "application/activity+json")
819 |> get(outbox_endpoint <> "?page=true")
820 |> json_response(200)
822 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
823 assert length(result["orderedItems"]) == 20
824 assert length(result_ids) == 20
825 assert result["next"]
826 assert String.starts_with?(result["next"], outbox_endpoint)
830 |> put_req_header("accept", "application/activity+json")
831 |> get(result["next"])
832 |> json_response(200)
834 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
835 assert length(result_next["orderedItems"]) == 6
836 assert length(result_next_ids) == 6
837 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
838 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
839 assert String.starts_with?(result["id"], outbox_endpoint)
843 |> put_req_header("accept", "application/activity+json")
844 |> get(result_next["id"])
845 |> json_response(200)
847 assert result_next == result_next_again
850 test "it returns 200 even if there're no activities", %{conn: conn} do
852 outbox_endpoint = user.ap_id <> "/outbox"
856 |> assign(:user, user)
857 |> put_req_header("accept", "application/activity+json")
858 |> get(outbox_endpoint)
860 result = json_response(conn, 200)
861 assert outbox_endpoint == result["id"]
864 test "it returns a note activity in a collection", %{conn: conn} do
865 note_activity = insert(:note_activity)
866 note_object = Object.normalize(note_activity)
867 user = User.get_cached_by_ap_id(note_activity.data["actor"])
871 |> assign(:user, user)
872 |> put_req_header("accept", "application/activity+json")
873 |> get("/users/#{user.nickname}/outbox?page=true")
875 assert response(conn, 200) =~ note_object.data["content"]
878 test "it returns an announce activity in a collection", %{conn: conn} do
879 announce_activity = insert(:announce_activity)
880 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
884 |> assign(:user, user)
885 |> put_req_header("accept", "application/activity+json")
886 |> get("/users/#{user.nickname}/outbox?page=true")
888 assert response(conn, 200) =~ announce_activity.data["object"]
892 describe "POST /users/:nickname/outbox (C2S)" do
893 setup do: clear_config([:instance, :limit])
898 "@context" => "https://www.w3.org/ns/activitystreams",
900 "object" => %{"type" => "Note", "content" => "AP C2S test"},
901 "to" => "https://www.w3.org/ns/activitystreams#Public",
907 test "it rejects posts from other users / unauthenticated users", %{
912 other_user = insert(:user)
913 conn = put_req_header(conn, "content-type", "application/activity+json")
916 |> post("/users/#{user.nickname}/outbox", activity)
917 |> json_response(403)
920 |> assign(:user, other_user)
921 |> post("/users/#{user.nickname}/outbox", activity)
922 |> json_response(403)
925 test "it inserts an incoming create activity into the database", %{
933 |> assign(:user, user)
934 |> put_req_header("content-type", "application/activity+json")
935 |> post("/users/#{user.nickname}/outbox", activity)
936 |> json_response(201)
938 assert Activity.get_by_ap_id(result["id"])
939 assert result["object"]
940 assert %Object{data: object} = Object.normalize(result["object"])
941 assert object["content"] == activity["object"]["content"]
944 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
949 |> put_in(["object", "type"], "Benis")
953 |> assign(:user, user)
954 |> put_req_header("content-type", "application/activity+json")
955 |> post("/users/#{user.nickname}/outbox", activity)
956 |> json_response(400)
959 test "it inserts an incoming sensitive activity into the database", %{
964 conn = assign(conn, :user, user)
965 object = Map.put(activity["object"], "sensitive", true)
966 activity = Map.put(activity, "object", object)
970 |> put_req_header("content-type", "application/activity+json")
971 |> post("/users/#{user.nickname}/outbox", activity)
972 |> json_response(201)
974 assert Activity.get_by_ap_id(response["id"])
975 assert response["object"]
976 assert %Object{data: response_object} = Object.normalize(response["object"])
977 assert response_object["sensitive"] == true
978 assert response_object["content"] == activity["object"]["content"]
982 |> put_req_header("accept", "application/activity+json")
983 |> get(response["id"])
984 |> json_response(200)
986 assert representation["object"]["sensitive"] == true
989 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
991 activity = Map.put(activity, "type", "BadType")
995 |> assign(:user, user)
996 |> put_req_header("content-type", "application/activity+json")
997 |> post("/users/#{user.nickname}/outbox", activity)
999 assert json_response(conn, 400)
1002 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1003 note_activity = insert(:note_activity)
1004 note_object = Object.normalize(note_activity)
1005 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1010 id: note_object.data["id"]
1016 |> assign(:user, user)
1017 |> put_req_header("content-type", "application/activity+json")
1018 |> post("/users/#{user.nickname}/outbox", data)
1020 result = json_response(conn, 201)
1021 assert Activity.get_by_ap_id(result["id"])
1023 assert object = Object.get_by_ap_id(note_object.data["id"])
1024 assert object.data["type"] == "Tombstone"
1027 test "it rejects delete activity of object from other actor", %{conn: conn} do
1028 note_activity = insert(:note_activity)
1029 note_object = Object.normalize(note_activity)
1030 user = insert(:user)
1035 id: note_object.data["id"]
1041 |> assign(:user, user)
1042 |> put_req_header("content-type", "application/activity+json")
1043 |> post("/users/#{user.nickname}/outbox", data)
1045 assert json_response(conn, 400)
1048 test "it increases like count when receiving a like action", %{conn: conn} do
1049 note_activity = insert(:note_activity)
1050 note_object = Object.normalize(note_activity)
1051 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1056 id: note_object.data["id"]
1062 |> assign(:user, user)
1063 |> put_req_header("content-type", "application/activity+json")
1064 |> post("/users/#{user.nickname}/outbox", data)
1066 result = json_response(conn, 201)
1067 assert Activity.get_by_ap_id(result["id"])
1069 assert object = Object.get_by_ap_id(note_object.data["id"])
1070 assert object.data["like_count"] == 1
1073 test "it doesn't spreads faulty attributedTo or actor fields", %{
1077 reimu = insert(:user, nickname: "reimu")
1078 cirno = insert(:user, nickname: "cirno")
1085 |> put_in(["object", "actor"], reimu.ap_id)
1086 |> put_in(["object", "attributedTo"], reimu.ap_id)
1087 |> put_in(["actor"], reimu.ap_id)
1088 |> put_in(["attributedTo"], reimu.ap_id)
1092 |> assign(:user, cirno)
1093 |> put_req_header("content-type", "application/activity+json")
1094 |> post("/users/#{reimu.nickname}/outbox", activity)
1095 |> json_response(403)
1099 |> assign(:user, cirno)
1100 |> put_req_header("content-type", "application/activity+json")
1101 |> post("/users/#{cirno.nickname}/outbox", activity)
1102 |> json_response(201)
1104 assert cirno_outbox["attributedTo"] == nil
1105 assert cirno_outbox["actor"] == cirno.ap_id
1107 assert cirno_object = Object.normalize(cirno_outbox["object"])
1108 assert cirno_object.data["actor"] == cirno.ap_id
1109 assert cirno_object.data["attributedTo"] == cirno.ap_id
1112 test "Character limitation", %{conn: conn, activity: activity} do
1113 Pleroma.Config.put([:instance, :limit], 5)
1114 user = insert(:user)
1118 |> assign(:user, user)
1119 |> put_req_header("content-type", "application/activity+json")
1120 |> post("/users/#{user.nickname}/outbox", activity)
1121 |> json_response(400)
1123 assert result == "Note is over the character limit"
1127 describe "/relay/followers" do
1128 test "it returns relay followers", %{conn: conn} do
1129 relay_actor = Relay.get_actor()
1130 user = insert(:user)
1131 User.follow(user, relay_actor)
1135 |> get("/relay/followers")
1136 |> json_response(200)
1138 assert result["first"]["orderedItems"] == [user.ap_id]
1141 test "on non-federating instance, it returns 404", %{conn: conn} do
1142 Config.put([:instance, :federating], false)
1143 user = insert(:user)
1146 |> assign(:user, user)
1147 |> get("/relay/followers")
1148 |> json_response(404)
1152 describe "/relay/following" do
1153 test "it returns relay following", %{conn: conn} do
1156 |> get("/relay/following")
1157 |> json_response(200)
1159 assert result["first"]["orderedItems"] == []
1162 test "on non-federating instance, it returns 404", %{conn: conn} do
1163 Config.put([:instance, :federating], false)
1164 user = insert(:user)
1167 |> assign(:user, user)
1168 |> get("/relay/following")
1169 |> json_response(404)
1173 describe "/users/:nickname/followers" do
1174 test "it returns the followers in a collection", %{conn: conn} do
1175 user = insert(:user)
1176 user_two = insert(:user)
1177 User.follow(user, user_two)
1181 |> assign(:user, user_two)
1182 |> get("/users/#{user_two.nickname}/followers")
1183 |> json_response(200)
1185 assert result["first"]["orderedItems"] == [user.ap_id]
1188 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1189 user = insert(:user)
1190 user_two = insert(:user, hide_followers: true)
1191 User.follow(user, user_two)
1195 |> assign(:user, user)
1196 |> get("/users/#{user_two.nickname}/followers")
1197 |> json_response(200)
1199 assert is_binary(result["first"])
1202 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1204 user = insert(:user)
1205 other_user = insert(:user, hide_followers: true)
1209 |> assign(:user, user)
1210 |> get("/users/#{other_user.nickname}/followers?page=1")
1212 assert result.status == 403
1213 assert result.resp_body == ""
1216 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1218 user = insert(:user, hide_followers: true)
1219 other_user = insert(:user)
1220 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1224 |> assign(:user, user)
1225 |> get("/users/#{user.nickname}/followers?page=1")
1226 |> json_response(200)
1228 assert result["totalItems"] == 1
1229 assert result["orderedItems"] == [other_user.ap_id]
1232 test "it works for more than 10 users", %{conn: conn} do
1233 user = insert(:user)
1235 Enum.each(1..15, fn _ ->
1236 other_user = insert(:user)
1237 User.follow(other_user, user)
1242 |> assign(:user, user)
1243 |> get("/users/#{user.nickname}/followers")
1244 |> json_response(200)
1246 assert length(result["first"]["orderedItems"]) == 10
1247 assert result["first"]["totalItems"] == 15
1248 assert result["totalItems"] == 15
1252 |> assign(:user, user)
1253 |> get("/users/#{user.nickname}/followers?page=2")
1254 |> json_response(200)
1256 assert length(result["orderedItems"]) == 5
1257 assert result["totalItems"] == 15
1260 test "does not require authentication", %{conn: conn} do
1261 user = insert(:user)
1264 |> get("/users/#{user.nickname}/followers")
1265 |> json_response(200)
1269 describe "/users/:nickname/following" do
1270 test "it returns the following in a collection", %{conn: conn} do
1271 user = insert(:user)
1272 user_two = insert(:user)
1273 User.follow(user, user_two)
1277 |> assign(:user, user)
1278 |> get("/users/#{user.nickname}/following")
1279 |> json_response(200)
1281 assert result["first"]["orderedItems"] == [user_two.ap_id]
1284 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1285 user = insert(:user)
1286 user_two = insert(:user, hide_follows: true)
1287 User.follow(user, user_two)
1291 |> assign(:user, user)
1292 |> get("/users/#{user_two.nickname}/following")
1293 |> json_response(200)
1295 assert is_binary(result["first"])
1298 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1300 user = insert(:user)
1301 user_two = insert(:user, hide_follows: true)
1305 |> assign(:user, user)
1306 |> get("/users/#{user_two.nickname}/following?page=1")
1308 assert result.status == 403
1309 assert result.resp_body == ""
1312 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1314 user = insert(:user, hide_follows: true)
1315 other_user = insert(:user)
1316 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1320 |> assign(:user, user)
1321 |> get("/users/#{user.nickname}/following?page=1")
1322 |> json_response(200)
1324 assert result["totalItems"] == 1
1325 assert result["orderedItems"] == [other_user.ap_id]
1328 test "it works for more than 10 users", %{conn: conn} do
1329 user = insert(:user)
1331 Enum.each(1..15, fn _ ->
1332 user = User.get_cached_by_id(user.id)
1333 other_user = insert(:user)
1334 User.follow(user, other_user)
1339 |> assign(:user, user)
1340 |> get("/users/#{user.nickname}/following")
1341 |> json_response(200)
1343 assert length(result["first"]["orderedItems"]) == 10
1344 assert result["first"]["totalItems"] == 15
1345 assert result["totalItems"] == 15
1349 |> assign(:user, user)
1350 |> get("/users/#{user.nickname}/following?page=2")
1351 |> json_response(200)
1353 assert length(result["orderedItems"]) == 5
1354 assert result["totalItems"] == 15
1357 test "does not require authentication", %{conn: conn} do
1358 user = insert(:user)
1361 |> get("/users/#{user.nickname}/following")
1362 |> json_response(200)
1366 describe "delivery tracking" do
1367 test "it tracks a signed object fetch", %{conn: conn} do
1368 user = insert(:user, local: false)
1369 activity = insert(:note_activity)
1370 object = Object.normalize(activity)
1372 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1375 |> put_req_header("accept", "application/activity+json")
1376 |> assign(:user, user)
1378 |> json_response(200)
1380 assert Delivery.get(object.id, user.id)
1383 test "it tracks a signed activity fetch", %{conn: conn} do
1384 user = insert(:user, local: false)
1385 activity = insert(:note_activity)
1386 object = Object.normalize(activity)
1388 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1391 |> put_req_header("accept", "application/activity+json")
1392 |> assign(:user, user)
1393 |> get(activity_path)
1394 |> json_response(200)
1396 assert Delivery.get(object.id, user.id)
1399 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1400 user = insert(:user, local: false)
1401 other_user = insert(:user, local: false)
1402 activity = insert(:note_activity)
1403 object = Object.normalize(activity)
1405 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1408 |> put_req_header("accept", "application/activity+json")
1409 |> assign(:user, user)
1411 |> json_response(200)
1414 |> put_req_header("accept", "application/activity+json")
1415 |> assign(:user, other_user)
1417 |> json_response(200)
1419 assert Delivery.get(object.id, user.id)
1420 assert Delivery.get(object.id, other_user.id)
1423 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1424 user = insert(:user, local: false)
1425 other_user = insert(:user, local: false)
1426 activity = insert(:note_activity)
1427 object = Object.normalize(activity)
1429 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1432 |> put_req_header("accept", "application/activity+json")
1433 |> assign(:user, user)
1434 |> get(activity_path)
1435 |> json_response(200)
1438 |> put_req_header("accept", "application/activity+json")
1439 |> assign(:user, other_user)
1440 |> get(activity_path)
1441 |> json_response(200)
1443 assert Delivery.get(object.id, user.id)
1444 assert Delivery.get(object.id, other_user.id)
1448 describe "Additional ActivityPub C2S endpoints" do
1449 test "GET /api/ap/whoami", %{conn: conn} do
1450 user = insert(:user)
1454 |> assign(:user, user)
1455 |> get("/api/ap/whoami")
1457 user = User.get_cached_by_id(user.id)
1459 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1462 |> get("/api/ap/whoami")
1463 |> json_response(403)
1466 setup do: clear_config([:media_proxy])
1467 setup do: clear_config([Pleroma.Upload])
1469 test "POST /api/ap/upload_media", %{conn: conn} do
1470 user = insert(:user)
1472 desc = "Description of the image"
1474 image = %Plug.Upload{
1475 content_type: "bad/content-type",
1476 path: Path.absname("test/fixtures/image.jpg"),
1477 filename: "an_image.png"
1482 |> assign(:user, user)
1483 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1484 |> json_response(:created)
1486 assert object["name"] == desc
1487 assert object["type"] == "Document"
1488 assert object["actor"] == user.ap_id
1489 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1490 assert is_binary(object_href)
1491 assert object_mediatype == "image/jpeg"
1492 assert String.ends_with?(object_href, ".jpg")
1494 activity_request = %{
1495 "@context" => "https://www.w3.org/ns/activitystreams",
1499 "content" => "AP C2S test, attachment",
1500 "attachment" => [object]
1502 "to" => "https://www.w3.org/ns/activitystreams#Public",
1508 |> assign(:user, user)
1509 |> post("/users/#{user.nickname}/outbox", activity_request)
1510 |> json_response(:created)
1512 assert activity_response["id"]
1513 assert activity_response["object"]
1514 assert activity_response["actor"] == user.ap_id
1516 assert %Object{data: %{"attachment" => [attachment]}} =
1517 Object.normalize(activity_response["object"])
1519 assert attachment["type"] == "Document"
1520 assert attachment["name"] == desc
1524 "href" => ^object_href,
1526 "mediaType" => ^object_mediatype
1528 ] = attachment["url"]
1530 # Fails if unauthenticated
1532 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1533 |> json_response(403)