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"
160 test "it requires authentication if instance is NOT federating", %{
169 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
172 ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user)
176 describe "mastodon compatibility routes" do
177 test "it returns a json representation of the object with accept application/json", %{
184 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
185 "actor" => Endpoint.url() <> "/users/raymoo",
186 "to" => [Pleroma.Constants.as_public()]
192 |> put_req_header("accept", "application/json")
193 |> get("/users/raymoo/statuses/999999999")
195 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
198 test "it returns a json representation of the activity with accept application/json", %{
205 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
206 "actor" => Endpoint.url() <> "/users/raymoo",
207 "to" => [Pleroma.Constants.as_public()]
213 "id" => object.data["id"] <> "/activity",
215 "object" => object.data["id"],
216 "actor" => object.data["actor"],
217 "to" => object.data["to"]
219 |> ActivityPub.persist(local: true)
223 |> put_req_header("accept", "application/json")
224 |> get("/users/raymoo/statuses/999999999/activity")
226 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
230 describe "/objects/:uuid" do
231 test "it returns a json representation of the object with accept application/json", %{
235 uuid = String.split(note.data["id"], "/") |> List.last()
239 |> put_req_header("accept", "application/json")
240 |> get("/objects/#{uuid}")
242 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
245 test "it returns a json representation of the object with accept application/activity+json",
248 uuid = String.split(note.data["id"], "/") |> List.last()
252 |> put_req_header("accept", "application/activity+json")
253 |> get("/objects/#{uuid}")
255 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
258 test "it returns a json representation of the object with accept application/ld+json", %{
262 uuid = String.split(note.data["id"], "/") |> List.last()
268 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
270 |> get("/objects/#{uuid}")
272 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
275 test "it returns 404 for non-public messages", %{conn: conn} do
276 note = insert(:direct_note)
277 uuid = String.split(note.data["id"], "/") |> List.last()
281 |> put_req_header("accept", "application/activity+json")
282 |> get("/objects/#{uuid}")
284 assert json_response(conn, 404)
287 test "it returns 404 for tombstone objects", %{conn: conn} do
288 tombstone = insert(:tombstone)
289 uuid = String.split(tombstone.data["id"], "/") |> List.last()
293 |> put_req_header("accept", "application/activity+json")
294 |> get("/objects/#{uuid}")
296 assert json_response(conn, 404)
299 test "it caches a response", %{conn: conn} do
301 uuid = String.split(note.data["id"], "/") |> List.last()
305 |> put_req_header("accept", "application/activity+json")
306 |> get("/objects/#{uuid}")
308 assert json_response(conn1, :ok)
309 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
313 |> put_req_header("accept", "application/activity+json")
314 |> get("/objects/#{uuid}")
316 assert json_response(conn1, :ok) == json_response(conn2, :ok)
317 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
320 test "cached purged after object deletion", %{conn: conn} do
322 uuid = String.split(note.data["id"], "/") |> List.last()
326 |> put_req_header("accept", "application/activity+json")
327 |> get("/objects/#{uuid}")
329 assert json_response(conn1, :ok)
330 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
336 |> put_req_header("accept", "application/activity+json")
337 |> get("/objects/#{uuid}")
339 assert "Not found" == json_response(conn2, :not_found)
342 test "it requires authentication if instance is NOT federating", %{
347 uuid = String.split(note.data["id"], "/") |> List.last()
349 conn = put_req_header(conn, "accept", "application/activity+json")
351 ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user)
355 describe "/activities/:uuid" do
356 test "it returns a json representation of the activity", %{conn: conn} do
357 activity = insert(:note_activity)
358 uuid = String.split(activity.data["id"], "/") |> List.last()
362 |> put_req_header("accept", "application/activity+json")
363 |> get("/activities/#{uuid}")
365 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
368 test "it returns 404 for non-public activities", %{conn: conn} do
369 activity = insert(:direct_note_activity)
370 uuid = String.split(activity.data["id"], "/") |> List.last()
374 |> put_req_header("accept", "application/activity+json")
375 |> get("/activities/#{uuid}")
377 assert json_response(conn, 404)
380 test "it caches a response", %{conn: conn} do
381 activity = insert(:note_activity)
382 uuid = String.split(activity.data["id"], "/") |> List.last()
386 |> put_req_header("accept", "application/activity+json")
387 |> get("/activities/#{uuid}")
389 assert json_response(conn1, :ok)
390 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
394 |> put_req_header("accept", "application/activity+json")
395 |> get("/activities/#{uuid}")
397 assert json_response(conn1, :ok) == json_response(conn2, :ok)
398 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
401 test "cached purged after activity deletion", %{conn: conn} do
403 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
405 uuid = String.split(activity.data["id"], "/") |> List.last()
409 |> put_req_header("accept", "application/activity+json")
410 |> get("/activities/#{uuid}")
412 assert json_response(conn1, :ok)
413 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
415 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
419 |> put_req_header("accept", "application/activity+json")
420 |> get("/activities/#{uuid}")
422 assert "Not found" == json_response(conn2, :not_found)
425 test "it requires authentication if instance is NOT federating", %{
429 activity = insert(:note_activity)
430 uuid = String.split(activity.data["id"], "/") |> List.last()
432 conn = put_req_header(conn, "accept", "application/activity+json")
434 ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user)
439 test "it inserts an incoming activity into the database", %{conn: conn} do
440 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
444 |> assign(:valid_signature, true)
445 |> put_req_header("content-type", "application/activity+json")
446 |> post("/inbox", data)
448 assert "ok" == json_response(conn, 200)
450 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
451 assert Activity.get_by_ap_id(data["id"])
454 @tag capture_log: true
455 test "it inserts an incoming activity into the database" <>
456 "even if we can't fetch the user but have it in our db",
460 ap_id: "https://mastodon.example.org/users/raymoo",
463 last_refreshed_at: nil
467 File.read!("test/fixtures/mastodon-post-activity.json")
469 |> Map.put("actor", user.ap_id)
470 |> put_in(["object", "attridbutedTo"], user.ap_id)
474 |> assign(:valid_signature, true)
475 |> put_req_header("content-type", "application/activity+json")
476 |> post("/inbox", data)
478 assert "ok" == json_response(conn, 200)
480 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
481 assert Activity.get_by_ap_id(data["id"])
484 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
485 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
487 sender_url = data["actor"]
488 Instances.set_consistently_unreachable(sender_url)
489 refute Instances.reachable?(sender_url)
493 |> assign(:valid_signature, true)
494 |> put_req_header("content-type", "application/activity+json")
495 |> post("/inbox", data)
497 assert "ok" == json_response(conn, 200)
498 assert Instances.reachable?(sender_url)
501 test "accept follow activity", %{conn: conn} do
502 Pleroma.Config.put([:instance, :federating], true)
503 relay = Relay.get_actor()
505 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
507 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
508 relay = refresh_record(relay)
511 File.read!("test/fixtures/relay/accept-follow.json")
512 |> String.replace("{{ap_id}}", relay.ap_id)
513 |> String.replace("{{activity_id}}", activity.data["id"])
517 |> assign(:valid_signature, true)
518 |> put_req_header("content-type", "application/activity+json")
519 |> post("/inbox", accept)
520 |> json_response(200)
522 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
524 assert Pleroma.FollowingRelationship.following?(
529 Mix.shell(Mix.Shell.Process)
532 Mix.shell(Mix.Shell.IO)
535 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
536 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
539 @tag capture_log: true
540 test "without valid signature, " <>
541 "it only accepts Create activities and requires enabled federation",
543 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
544 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
546 conn = put_req_header(conn, "content-type", "application/activity+json")
548 Config.put([:instance, :federating], false)
551 |> post("/inbox", data)
552 |> json_response(403)
555 |> post("/inbox", non_create_data)
556 |> json_response(403)
558 Config.put([:instance, :federating], true)
560 ret_conn = post(conn, "/inbox", data)
561 assert "ok" == json_response(ret_conn, 200)
564 |> post("/inbox", non_create_data)
565 |> json_response(400)
569 describe "/users/:nickname/inbox" do
572 File.read!("test/fixtures/mastodon-post-activity.json")
578 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
580 data = Map.put(data, "bcc", [user.ap_id])
584 |> assign(:valid_signature, true)
585 |> put_req_header("content-type", "application/activity+json")
586 |> post("/users/#{user.nickname}/inbox", data)
588 assert "ok" == json_response(conn, 200)
589 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
590 assert Activity.get_by_ap_id(data["id"])
593 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
597 Map.put(data, "to", user.ap_id)
602 |> assign(:valid_signature, true)
603 |> put_req_header("content-type", "application/activity+json")
604 |> post("/users/#{user.nickname}/inbox", data)
606 assert "ok" == json_response(conn, 200)
607 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
608 assert Activity.get_by_ap_id(data["id"])
611 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
615 Map.put(data, "cc", user.ap_id)
620 |> assign(:valid_signature, true)
621 |> put_req_header("content-type", "application/activity+json")
622 |> post("/users/#{user.nickname}/inbox", data)
624 assert "ok" == json_response(conn, 200)
625 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
626 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
627 assert user.ap_id in activity.recipients
630 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
634 Map.put(data, "bcc", user.ap_id)
640 |> assign(:valid_signature, true)
641 |> put_req_header("content-type", "application/activity+json")
642 |> post("/users/#{user.nickname}/inbox", data)
644 assert "ok" == json_response(conn, 200)
645 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
646 assert Activity.get_by_ap_id(data["id"])
649 test "it accepts announces with to as string instead of array", %{conn: conn} do
652 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
653 announcer = insert(:user, local: false)
656 "@context" => "https://www.w3.org/ns/activitystreams",
657 "actor" => announcer.ap_id,
658 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
659 "object" => post.data["object"],
660 "to" => "https://www.w3.org/ns/activitystreams#Public",
661 "cc" => [user.ap_id],
667 |> assign(:valid_signature, true)
668 |> put_req_header("content-type", "application/activity+json")
669 |> post("/users/#{user.nickname}/inbox", data)
671 assert "ok" == json_response(conn, 200)
672 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
673 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
674 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
677 test "it accepts messages from actors that are followed by the user", %{
681 recipient = insert(:user)
682 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
684 {:ok, recipient} = User.follow(recipient, actor)
688 |> Map.put("attributedTo", actor.ap_id)
692 |> Map.put("actor", actor.ap_id)
693 |> Map.put("object", object)
697 |> assign(:valid_signature, true)
698 |> put_req_header("content-type", "application/activity+json")
699 |> post("/users/#{recipient.nickname}/inbox", data)
701 assert "ok" == json_response(conn, 200)
702 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
703 assert Activity.get_by_ap_id(data["id"])
706 test "it rejects reads from other users", %{conn: conn} do
708 other_user = insert(:user)
712 |> assign(:user, other_user)
713 |> put_req_header("accept", "application/activity+json")
714 |> get("/users/#{user.nickname}/inbox")
716 assert json_response(conn, 403)
719 test "it returns a note activity in a collection", %{conn: conn} do
720 note_activity = insert(:direct_note_activity)
721 note_object = Object.normalize(note_activity)
722 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
726 |> assign(:user, user)
727 |> put_req_header("accept", "application/activity+json")
728 |> get("/users/#{user.nickname}/inbox?page=true")
730 assert response(conn, 200) =~ note_object.data["content"]
733 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
735 data = Map.put(data, "bcc", [user.ap_id])
737 sender_host = URI.parse(data["actor"]).host
738 Instances.set_consistently_unreachable(sender_host)
739 refute Instances.reachable?(sender_host)
743 |> assign(:valid_signature, true)
744 |> put_req_header("content-type", "application/activity+json")
745 |> post("/users/#{user.nickname}/inbox", data)
747 assert "ok" == json_response(conn, 200)
748 assert Instances.reachable?(sender_host)
751 test "it removes all follower collections but actor's", %{conn: conn} do
752 [actor, recipient] = insert_pair(:user)
755 File.read!("test/fixtures/activitypub-client-post-activity.json")
758 object = Map.put(data["object"], "attributedTo", actor.ap_id)
762 |> Map.put("id", Utils.generate_object_id())
763 |> Map.put("actor", actor.ap_id)
764 |> Map.put("object", object)
766 recipient.follower_address,
767 actor.follower_address
771 recipient.follower_address,
772 "https://www.w3.org/ns/activitystreams#Public"
776 |> assign(:valid_signature, true)
777 |> put_req_header("content-type", "application/activity+json")
778 |> post("/users/#{recipient.nickname}/inbox", data)
779 |> json_response(200)
781 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
783 activity = Activity.get_by_ap_id(data["id"])
786 assert actor.follower_address in activity.recipients
787 assert actor.follower_address in activity.data["cc"]
789 refute recipient.follower_address in activity.recipients
790 refute recipient.follower_address in activity.data["cc"]
791 refute recipient.follower_address in activity.data["to"]
794 test "it requires authentication", %{conn: conn} do
796 conn = put_req_header(conn, "accept", "application/activity+json")
798 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
799 assert json_response(ret_conn, 403)
803 |> assign(:user, user)
804 |> get("/users/#{user.nickname}/inbox")
806 assert json_response(ret_conn, 200)
810 describe "GET /users/:nickname/outbox" do
811 test "it paginates correctly", %{conn: conn} do
813 conn = assign(conn, :user, user)
814 outbox_endpoint = user.ap_id <> "/outbox"
818 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
824 |> put_req_header("accept", "application/activity+json")
825 |> get(outbox_endpoint <> "?page=true")
826 |> json_response(200)
828 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
829 assert length(result["orderedItems"]) == 20
830 assert length(result_ids) == 20
831 assert result["next"]
832 assert String.starts_with?(result["next"], outbox_endpoint)
836 |> put_req_header("accept", "application/activity+json")
837 |> get(result["next"])
838 |> json_response(200)
840 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
841 assert length(result_next["orderedItems"]) == 6
842 assert length(result_next_ids) == 6
843 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
844 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
845 assert String.starts_with?(result["id"], outbox_endpoint)
849 |> put_req_header("accept", "application/activity+json")
850 |> get(result_next["id"])
851 |> json_response(200)
853 assert result_next == result_next_again
856 test "it returns 200 even if there're no activities", %{conn: conn} do
858 outbox_endpoint = user.ap_id <> "/outbox"
862 |> assign(:user, user)
863 |> put_req_header("accept", "application/activity+json")
864 |> get(outbox_endpoint)
866 result = json_response(conn, 200)
867 assert outbox_endpoint == result["id"]
870 test "it returns a note activity in a collection", %{conn: conn} do
871 note_activity = insert(:note_activity)
872 note_object = Object.normalize(note_activity)
873 user = User.get_cached_by_ap_id(note_activity.data["actor"])
877 |> assign(:user, user)
878 |> put_req_header("accept", "application/activity+json")
879 |> get("/users/#{user.nickname}/outbox?page=true")
881 assert response(conn, 200) =~ note_object.data["content"]
884 test "it returns an announce activity in a collection", %{conn: conn} do
885 announce_activity = insert(:announce_activity)
886 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
890 |> assign(:user, user)
891 |> put_req_header("accept", "application/activity+json")
892 |> get("/users/#{user.nickname}/outbox?page=true")
894 assert response(conn, 200) =~ announce_activity.data["object"]
897 test "it requires authentication if instance is NOT federating", %{
901 conn = put_req_header(conn, "accept", "application/activity+json")
903 ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user)
907 describe "POST /users/:nickname/outbox (C2S)" do
908 setup do: clear_config([:instance, :limit])
913 "@context" => "https://www.w3.org/ns/activitystreams",
915 "object" => %{"type" => "Note", "content" => "AP C2S test"},
916 "to" => "https://www.w3.org/ns/activitystreams#Public",
922 test "it rejects posts from other users / unauthenticated users", %{
927 other_user = insert(:user)
928 conn = put_req_header(conn, "content-type", "application/activity+json")
931 |> post("/users/#{user.nickname}/outbox", activity)
932 |> json_response(403)
935 |> assign(:user, other_user)
936 |> post("/users/#{user.nickname}/outbox", activity)
937 |> json_response(403)
940 test "it inserts an incoming create activity into the database", %{
948 |> assign(:user, user)
949 |> put_req_header("content-type", "application/activity+json")
950 |> post("/users/#{user.nickname}/outbox", activity)
951 |> json_response(201)
953 assert Activity.get_by_ap_id(result["id"])
954 assert result["object"]
955 assert %Object{data: object} = Object.normalize(result["object"])
956 assert object["content"] == activity["object"]["content"]
959 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
964 |> put_in(["object", "type"], "Benis")
968 |> assign(:user, user)
969 |> put_req_header("content-type", "application/activity+json")
970 |> post("/users/#{user.nickname}/outbox", activity)
971 |> json_response(400)
974 test "it inserts an incoming sensitive activity into the database", %{
979 conn = assign(conn, :user, user)
980 object = Map.put(activity["object"], "sensitive", true)
981 activity = Map.put(activity, "object", object)
985 |> put_req_header("content-type", "application/activity+json")
986 |> post("/users/#{user.nickname}/outbox", activity)
987 |> json_response(201)
989 assert Activity.get_by_ap_id(response["id"])
990 assert response["object"]
991 assert %Object{data: response_object} = Object.normalize(response["object"])
992 assert response_object["sensitive"] == true
993 assert response_object["content"] == activity["object"]["content"]
997 |> put_req_header("accept", "application/activity+json")
998 |> get(response["id"])
999 |> json_response(200)
1001 assert representation["object"]["sensitive"] == true
1004 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1005 user = insert(:user)
1006 activity = Map.put(activity, "type", "BadType")
1010 |> assign(:user, user)
1011 |> put_req_header("content-type", "application/activity+json")
1012 |> post("/users/#{user.nickname}/outbox", activity)
1014 assert json_response(conn, 400)
1017 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1018 note_activity = insert(:note_activity)
1019 note_object = Object.normalize(note_activity)
1020 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1025 id: note_object.data["id"]
1031 |> assign(:user, user)
1032 |> put_req_header("content-type", "application/activity+json")
1033 |> post("/users/#{user.nickname}/outbox", data)
1035 result = json_response(conn, 201)
1036 assert Activity.get_by_ap_id(result["id"])
1038 assert object = Object.get_by_ap_id(note_object.data["id"])
1039 assert object.data["type"] == "Tombstone"
1042 test "it rejects delete activity of object from other actor", %{conn: conn} do
1043 note_activity = insert(:note_activity)
1044 note_object = Object.normalize(note_activity)
1045 user = insert(:user)
1050 id: note_object.data["id"]
1056 |> assign(:user, user)
1057 |> put_req_header("content-type", "application/activity+json")
1058 |> post("/users/#{user.nickname}/outbox", data)
1060 assert json_response(conn, 400)
1063 test "it increases like count when receiving a like action", %{conn: conn} do
1064 note_activity = insert(:note_activity)
1065 note_object = Object.normalize(note_activity)
1066 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1071 id: note_object.data["id"]
1077 |> assign(:user, user)
1078 |> put_req_header("content-type", "application/activity+json")
1079 |> post("/users/#{user.nickname}/outbox", data)
1081 result = json_response(conn, 201)
1082 assert Activity.get_by_ap_id(result["id"])
1084 assert object = Object.get_by_ap_id(note_object.data["id"])
1085 assert object.data["like_count"] == 1
1088 test "it doesn't spreads faulty attributedTo or actor fields", %{
1092 reimu = insert(:user, nickname: "reimu")
1093 cirno = insert(:user, nickname: "cirno")
1100 |> put_in(["object", "actor"], reimu.ap_id)
1101 |> put_in(["object", "attributedTo"], reimu.ap_id)
1102 |> put_in(["actor"], reimu.ap_id)
1103 |> put_in(["attributedTo"], reimu.ap_id)
1107 |> assign(:user, cirno)
1108 |> put_req_header("content-type", "application/activity+json")
1109 |> post("/users/#{reimu.nickname}/outbox", activity)
1110 |> json_response(403)
1114 |> assign(:user, cirno)
1115 |> put_req_header("content-type", "application/activity+json")
1116 |> post("/users/#{cirno.nickname}/outbox", activity)
1117 |> json_response(201)
1119 assert cirno_outbox["attributedTo"] == nil
1120 assert cirno_outbox["actor"] == cirno.ap_id
1122 assert cirno_object = Object.normalize(cirno_outbox["object"])
1123 assert cirno_object.data["actor"] == cirno.ap_id
1124 assert cirno_object.data["attributedTo"] == cirno.ap_id
1127 test "Character limitation", %{conn: conn, activity: activity} do
1128 Pleroma.Config.put([:instance, :limit], 5)
1129 user = insert(:user)
1133 |> assign(:user, user)
1134 |> put_req_header("content-type", "application/activity+json")
1135 |> post("/users/#{user.nickname}/outbox", activity)
1136 |> json_response(400)
1138 assert result == "Note is over the character limit"
1142 describe "/relay/followers" do
1143 test "it returns relay followers", %{conn: conn} do
1144 relay_actor = Relay.get_actor()
1145 user = insert(:user)
1146 User.follow(user, relay_actor)
1150 |> get("/relay/followers")
1151 |> json_response(200)
1153 assert result["first"]["orderedItems"] == [user.ap_id]
1156 test "on non-federating instance, it returns 404", %{conn: conn} do
1157 Config.put([:instance, :federating], false)
1158 user = insert(:user)
1161 |> assign(:user, user)
1162 |> get("/relay/followers")
1163 |> json_response(404)
1167 describe "/relay/following" do
1168 test "it returns relay following", %{conn: conn} do
1171 |> get("/relay/following")
1172 |> json_response(200)
1174 assert result["first"]["orderedItems"] == []
1177 test "on non-federating instance, it returns 404", %{conn: conn} do
1178 Config.put([:instance, :federating], false)
1179 user = insert(:user)
1182 |> assign(:user, user)
1183 |> get("/relay/following")
1184 |> json_response(404)
1188 describe "/users/:nickname/followers" do
1189 test "it returns the followers in a collection", %{conn: conn} do
1190 user = insert(:user)
1191 user_two = insert(:user)
1192 User.follow(user, user_two)
1196 |> assign(:user, user_two)
1197 |> get("/users/#{user_two.nickname}/followers")
1198 |> json_response(200)
1200 assert result["first"]["orderedItems"] == [user.ap_id]
1203 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1204 user = insert(:user)
1205 user_two = insert(:user, hide_followers: true)
1206 User.follow(user, user_two)
1210 |> assign(:user, user)
1211 |> get("/users/#{user_two.nickname}/followers")
1212 |> json_response(200)
1214 assert is_binary(result["first"])
1217 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1219 user = insert(:user)
1220 other_user = insert(:user, hide_followers: true)
1224 |> assign(:user, user)
1225 |> get("/users/#{other_user.nickname}/followers?page=1")
1227 assert result.status == 403
1228 assert result.resp_body == ""
1231 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1233 user = insert(:user, hide_followers: true)
1234 other_user = insert(:user)
1235 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1239 |> assign(:user, user)
1240 |> get("/users/#{user.nickname}/followers?page=1")
1241 |> json_response(200)
1243 assert result["totalItems"] == 1
1244 assert result["orderedItems"] == [other_user.ap_id]
1247 test "it works for more than 10 users", %{conn: conn} do
1248 user = insert(:user)
1250 Enum.each(1..15, fn _ ->
1251 other_user = insert(:user)
1252 User.follow(other_user, user)
1257 |> assign(:user, user)
1258 |> get("/users/#{user.nickname}/followers")
1259 |> json_response(200)
1261 assert length(result["first"]["orderedItems"]) == 10
1262 assert result["first"]["totalItems"] == 15
1263 assert result["totalItems"] == 15
1267 |> assign(:user, user)
1268 |> get("/users/#{user.nickname}/followers?page=2")
1269 |> json_response(200)
1271 assert length(result["orderedItems"]) == 5
1272 assert result["totalItems"] == 15
1275 test "does not require authentication", %{conn: conn} do
1276 user = insert(:user)
1279 |> get("/users/#{user.nickname}/followers")
1280 |> json_response(200)
1284 describe "/users/:nickname/following" do
1285 test "it returns the following in a collection", %{conn: conn} do
1286 user = insert(:user)
1287 user_two = insert(:user)
1288 User.follow(user, user_two)
1292 |> assign(:user, user)
1293 |> get("/users/#{user.nickname}/following")
1294 |> json_response(200)
1296 assert result["first"]["orderedItems"] == [user_two.ap_id]
1299 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1300 user = insert(:user)
1301 user_two = insert(:user, hide_follows: true)
1302 User.follow(user, user_two)
1306 |> assign(:user, user)
1307 |> get("/users/#{user_two.nickname}/following")
1308 |> json_response(200)
1310 assert is_binary(result["first"])
1313 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1315 user = insert(:user)
1316 user_two = insert(:user, hide_follows: true)
1320 |> assign(:user, user)
1321 |> get("/users/#{user_two.nickname}/following?page=1")
1323 assert result.status == 403
1324 assert result.resp_body == ""
1327 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1329 user = insert(:user, hide_follows: true)
1330 other_user = insert(:user)
1331 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1335 |> assign(:user, user)
1336 |> get("/users/#{user.nickname}/following?page=1")
1337 |> json_response(200)
1339 assert result["totalItems"] == 1
1340 assert result["orderedItems"] == [other_user.ap_id]
1343 test "it works for more than 10 users", %{conn: conn} do
1344 user = insert(:user)
1346 Enum.each(1..15, fn _ ->
1347 user = User.get_cached_by_id(user.id)
1348 other_user = insert(:user)
1349 User.follow(user, other_user)
1354 |> assign(:user, user)
1355 |> get("/users/#{user.nickname}/following")
1356 |> json_response(200)
1358 assert length(result["first"]["orderedItems"]) == 10
1359 assert result["first"]["totalItems"] == 15
1360 assert result["totalItems"] == 15
1364 |> assign(:user, user)
1365 |> get("/users/#{user.nickname}/following?page=2")
1366 |> json_response(200)
1368 assert length(result["orderedItems"]) == 5
1369 assert result["totalItems"] == 15
1372 test "does not require authentication", %{conn: conn} do
1373 user = insert(:user)
1376 |> get("/users/#{user.nickname}/following")
1377 |> json_response(200)
1381 describe "delivery tracking" do
1382 test "it tracks a signed object fetch", %{conn: conn} do
1383 user = insert(:user, local: false)
1384 activity = insert(:note_activity)
1385 object = Object.normalize(activity)
1387 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1390 |> put_req_header("accept", "application/activity+json")
1391 |> assign(:user, user)
1393 |> json_response(200)
1395 assert Delivery.get(object.id, user.id)
1398 test "it tracks a signed activity fetch", %{conn: conn} do
1399 user = insert(:user, local: false)
1400 activity = insert(:note_activity)
1401 object = Object.normalize(activity)
1403 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1406 |> put_req_header("accept", "application/activity+json")
1407 |> assign(:user, user)
1408 |> get(activity_path)
1409 |> json_response(200)
1411 assert Delivery.get(object.id, user.id)
1414 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1415 user = insert(:user, local: false)
1416 other_user = insert(:user, local: false)
1417 activity = insert(:note_activity)
1418 object = Object.normalize(activity)
1420 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1423 |> put_req_header("accept", "application/activity+json")
1424 |> assign(:user, user)
1426 |> json_response(200)
1429 |> put_req_header("accept", "application/activity+json")
1430 |> assign(:user, other_user)
1432 |> json_response(200)
1434 assert Delivery.get(object.id, user.id)
1435 assert Delivery.get(object.id, other_user.id)
1438 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1439 user = insert(:user, local: false)
1440 other_user = insert(:user, local: false)
1441 activity = insert(:note_activity)
1442 object = Object.normalize(activity)
1444 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1447 |> put_req_header("accept", "application/activity+json")
1448 |> assign(:user, user)
1449 |> get(activity_path)
1450 |> json_response(200)
1453 |> put_req_header("accept", "application/activity+json")
1454 |> assign(:user, other_user)
1455 |> get(activity_path)
1456 |> json_response(200)
1458 assert Delivery.get(object.id, user.id)
1459 assert Delivery.get(object.id, other_user.id)
1463 describe "Additional ActivityPub C2S endpoints" do
1464 test "GET /api/ap/whoami", %{conn: conn} do
1465 user = insert(:user)
1469 |> assign(:user, user)
1470 |> get("/api/ap/whoami")
1472 user = User.get_cached_by_id(user.id)
1474 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1477 |> get("/api/ap/whoami")
1478 |> json_response(403)
1481 setup do: clear_config([:media_proxy])
1482 setup do: clear_config([Pleroma.Upload])
1484 test "POST /api/ap/upload_media", %{conn: conn} do
1485 user = insert(:user)
1487 desc = "Description of the image"
1489 image = %Plug.Upload{
1490 content_type: "image/jpg",
1491 path: Path.absname("test/fixtures/image.jpg"),
1492 filename: "an_image.jpg"
1497 |> assign(:user, user)
1498 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1499 |> json_response(:created)
1501 assert object["name"] == desc
1502 assert object["type"] == "Document"
1503 assert object["actor"] == user.ap_id
1504 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1505 assert is_binary(object_href)
1506 assert object_mediatype == "image/jpeg"
1508 activity_request = %{
1509 "@context" => "https://www.w3.org/ns/activitystreams",
1513 "content" => "AP C2S test, attachment",
1514 "attachment" => [object]
1516 "to" => "https://www.w3.org/ns/activitystreams#Public",
1522 |> assign(:user, user)
1523 |> post("/users/#{user.nickname}/outbox", activity_request)
1524 |> json_response(:created)
1526 assert activity_response["id"]
1527 assert activity_response["object"]
1528 assert activity_response["actor"] == user.ap_id
1530 assert %Object{data: %{"attachment" => [attachment]}} =
1531 Object.normalize(activity_response["object"])
1533 assert attachment["type"] == "Document"
1534 assert attachment["name"] == desc
1538 "href" => ^object_href,
1540 "mediaType" => ^object_mediatype
1542 ] = attachment["url"]
1544 # Fails if unauthenticated
1546 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1547 |> json_response(403)