1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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, fetch: 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") |> Jason.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") |> Jason.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") |> Jason.decode!()
538 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.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, actor} = 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, fetch: false)
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)
803 @tag capture_log: true
804 test "forwarded report", %{conn: conn} do
805 admin = insert(:user, is_admin: true)
806 actor = insert(:user, local: false)
807 remote_domain = URI.parse(actor.ap_id).host
808 reported_user = insert(:user)
810 note = insert(:note_activity, user: reported_user)
814 "https://www.w3.org/ns/activitystreams",
815 "https://#{remote_domain}/schemas/litepub-0.1.jsonld",
820 "actor" => actor.ap_id,
825 "context" => "context",
826 "id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
827 "nickname" => reported_user.nickname,
832 "actor_type" => "Person",
833 "approval_pending" => false,
835 "confirmation_pending" => false,
836 "deactivated" => false,
837 "display_name" => "test user",
838 "id" => reported_user.id,
840 "nickname" => reported_user.nickname,
841 "registration_reason" => nil,
847 "url" => reported_user.ap_id
850 "id" => note.data["id"],
851 "published" => note.data["published"],
855 "published" => note.data["published"],
862 |> assign(:valid_signature, true)
863 |> put_req_header("content-type", "application/activity+json")
864 |> post("/users/#{reported_user.nickname}/inbox", data)
865 |> json_response(200)
867 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
869 assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
871 ObanHelpers.perform_all()
873 Swoosh.TestAssertions.assert_email_sent(
874 to: {admin.name, admin.email},
875 html_body: ~r/Reported Account:/i
879 @tag capture_log: true
880 test "forwarded report from mastodon", %{conn: conn} do
881 admin = insert(:user, is_admin: true)
882 actor = insert(:user, local: false)
883 remote_domain = URI.parse(actor.ap_id).host
884 remote_actor = "https://#{remote_domain}/actor"
885 [reported_user, another] = insert_list(2, :user)
887 note = insert(:note_activity, user: reported_user)
889 Pleroma.Web.CommonAPI.favorite(another, note.id)
892 "test/fixtures/mastodon/application_actor.json"
894 |> String.replace("{{DOMAIN}}", remote_domain)
896 Tesla.Mock.mock(fn %{url: ^remote_actor} ->
899 body: mock_json_body,
900 headers: [{"content-type", "application/activity+json"}]
905 "@context" => "https://www.w3.org/ns/activitystreams",
906 "actor" => remote_actor,
907 "content" => "test report",
908 "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
909 "nickname" => reported_user.nickname,
918 |> assign(:valid_signature, true)
919 |> put_req_header("content-type", "application/activity+json")
920 |> post("/users/#{reported_user.nickname}/inbox", data)
921 |> json_response(200)
923 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
925 flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
926 reported_user_ap_id = reported_user.ap_id
928 [^reported_user_ap_id, flag_data] = flag_activity.data["object"]
930 Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
931 ObanHelpers.perform_all()
933 Swoosh.TestAssertions.assert_email_sent(
934 to: {admin.name, admin.email},
935 html_body: ~r/#{note.data["object"]}/i
940 describe "GET /users/:nickname/outbox" do
941 test "it paginates correctly", %{conn: conn} do
943 conn = assign(conn, :user, user)
944 outbox_endpoint = user.ap_id <> "/outbox"
948 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
954 |> put_req_header("accept", "application/activity+json")
955 |> get(outbox_endpoint <> "?page=true")
956 |> json_response(200)
958 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
959 assert length(result["orderedItems"]) == 20
960 assert length(result_ids) == 20
961 assert result["next"]
962 assert String.starts_with?(result["next"], outbox_endpoint)
966 |> put_req_header("accept", "application/activity+json")
967 |> get(result["next"])
968 |> json_response(200)
970 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
971 assert length(result_next["orderedItems"]) == 6
972 assert length(result_next_ids) == 6
973 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
974 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
975 assert String.starts_with?(result["id"], outbox_endpoint)
979 |> put_req_header("accept", "application/activity+json")
980 |> get(result_next["id"])
981 |> json_response(200)
983 assert result_next == result_next_again
986 test "it returns 200 even if there're no activities", %{conn: conn} do
988 outbox_endpoint = user.ap_id <> "/outbox"
992 |> assign(:user, user)
993 |> put_req_header("accept", "application/activity+json")
994 |> get(outbox_endpoint)
996 result = json_response(conn, 200)
997 assert outbox_endpoint == result["id"]
1000 test "it returns a note activity in a collection", %{conn: conn} do
1001 note_activity = insert(:note_activity)
1002 note_object = Object.normalize(note_activity, fetch: false)
1003 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1007 |> assign(:user, user)
1008 |> put_req_header("accept", "application/activity+json")
1009 |> get("/users/#{user.nickname}/outbox?page=true")
1011 assert response(conn, 200) =~ note_object.data["content"]
1014 test "it returns an announce activity in a collection", %{conn: conn} do
1015 announce_activity = insert(:announce_activity)
1016 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
1020 |> assign(:user, user)
1021 |> put_req_header("accept", "application/activity+json")
1022 |> get("/users/#{user.nickname}/outbox?page=true")
1024 assert response(conn, 200) =~ announce_activity.data["object"]
1027 test "It returns poll Answers when authenticated", %{conn: conn} do
1028 poller = insert(:user)
1029 voter = insert(:user)
1032 CommonAPI.post(poller, %{
1034 poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
1037 assert question = Object.normalize(activity, fetch: false)
1039 {:ok, [activity], _object} = CommonAPI.vote(voter, question, [1])
1043 |> assign(:user, voter)
1044 |> put_req_header("accept", "application/activity+json")
1045 |> get(voter.ap_id <> "/outbox?page=true")
1046 |> json_response(200)
1048 assert [answer_outbox] = outbox_get["orderedItems"]
1049 assert answer_outbox["id"] == activity.data["id"]
1053 describe "POST /users/:nickname/outbox (C2S)" do
1054 setup do: clear_config([:instance, :limit])
1059 "@context" => "https://www.w3.org/ns/activitystreams",
1061 "object" => %{"type" => "Note", "content" => "AP C2S test"},
1062 "to" => "https://www.w3.org/ns/activitystreams#Public",
1068 test "it rejects posts from other users / unauthenticated users", %{
1072 user = insert(:user)
1073 other_user = insert(:user)
1074 conn = put_req_header(conn, "content-type", "application/activity+json")
1077 |> post("/users/#{user.nickname}/outbox", activity)
1078 |> json_response(403)
1081 |> assign(:user, other_user)
1082 |> post("/users/#{user.nickname}/outbox", activity)
1083 |> json_response(403)
1086 test "it inserts an incoming create activity into the database", %{
1090 user = insert(:user)
1094 |> assign(:user, user)
1095 |> put_req_header("content-type", "application/activity+json")
1096 |> post("/users/#{user.nickname}/outbox", activity)
1097 |> json_response(201)
1099 assert Activity.get_by_ap_id(result["id"])
1100 assert result["object"]
1101 assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
1102 assert object["content"] == activity["object"]["content"]
1105 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1106 user = insert(:user)
1110 |> put_in(["object", "type"], "Benis")
1114 |> assign(:user, user)
1115 |> put_req_header("content-type", "application/activity+json")
1116 |> post("/users/#{user.nickname}/outbox", activity)
1117 |> json_response(400)
1120 test "it inserts an incoming sensitive activity into the database", %{
1124 user = insert(:user)
1125 conn = assign(conn, :user, user)
1126 object = Map.put(activity["object"], "sensitive", true)
1127 activity = Map.put(activity, "object", object)
1131 |> put_req_header("content-type", "application/activity+json")
1132 |> post("/users/#{user.nickname}/outbox", activity)
1133 |> json_response(201)
1135 assert Activity.get_by_ap_id(response["id"])
1136 assert response["object"]
1137 assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false)
1138 assert response_object["sensitive"] == true
1139 assert response_object["content"] == activity["object"]["content"]
1143 |> put_req_header("accept", "application/activity+json")
1144 |> get(response["id"])
1145 |> json_response(200)
1147 assert representation["object"]["sensitive"] == true
1150 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1151 user = insert(:user)
1152 activity = Map.put(activity, "type", "BadType")
1156 |> assign(:user, user)
1157 |> put_req_header("content-type", "application/activity+json")
1158 |> post("/users/#{user.nickname}/outbox", activity)
1160 assert json_response(conn, 400)
1163 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1164 note_activity = insert(:note_activity)
1165 note_object = Object.normalize(note_activity, fetch: false)
1166 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1171 id: note_object.data["id"]
1177 |> assign(:user, user)
1178 |> put_req_header("content-type", "application/activity+json")
1179 |> post("/users/#{user.nickname}/outbox", data)
1181 result = json_response(conn, 201)
1182 assert Activity.get_by_ap_id(result["id"])
1184 assert object = Object.get_by_ap_id(note_object.data["id"])
1185 assert object.data["type"] == "Tombstone"
1188 test "it rejects delete activity of object from other actor", %{conn: conn} do
1189 note_activity = insert(:note_activity)
1190 note_object = Object.normalize(note_activity, fetch: false)
1191 user = insert(:user)
1196 id: note_object.data["id"]
1202 |> assign(:user, user)
1203 |> put_req_header("content-type", "application/activity+json")
1204 |> post("/users/#{user.nickname}/outbox", data)
1206 assert json_response(conn, 400)
1209 test "it increases like count when receiving a like action", %{conn: conn} do
1210 note_activity = insert(:note_activity)
1211 note_object = Object.normalize(note_activity, fetch: false)
1212 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1217 id: note_object.data["id"]
1223 |> assign(:user, user)
1224 |> put_req_header("content-type", "application/activity+json")
1225 |> post("/users/#{user.nickname}/outbox", data)
1227 result = json_response(conn, 201)
1228 assert Activity.get_by_ap_id(result["id"])
1230 assert object = Object.get_by_ap_id(note_object.data["id"])
1231 assert object.data["like_count"] == 1
1234 test "it doesn't spreads faulty attributedTo or actor fields", %{
1238 reimu = insert(:user, nickname: "reimu")
1239 cirno = insert(:user, nickname: "cirno")
1246 |> put_in(["object", "actor"], reimu.ap_id)
1247 |> put_in(["object", "attributedTo"], reimu.ap_id)
1248 |> put_in(["actor"], reimu.ap_id)
1249 |> put_in(["attributedTo"], reimu.ap_id)
1253 |> assign(:user, cirno)
1254 |> put_req_header("content-type", "application/activity+json")
1255 |> post("/users/#{reimu.nickname}/outbox", activity)
1256 |> json_response(403)
1260 |> assign(:user, cirno)
1261 |> put_req_header("content-type", "application/activity+json")
1262 |> post("/users/#{cirno.nickname}/outbox", activity)
1263 |> json_response(201)
1265 assert cirno_outbox["attributedTo"] == nil
1266 assert cirno_outbox["actor"] == cirno.ap_id
1268 assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false)
1269 assert cirno_object.data["actor"] == cirno.ap_id
1270 assert cirno_object.data["attributedTo"] == cirno.ap_id
1273 test "Character limitation", %{conn: conn, activity: activity} do
1274 Pleroma.Config.put([:instance, :limit], 5)
1275 user = insert(:user)
1279 |> assign(:user, user)
1280 |> put_req_header("content-type", "application/activity+json")
1281 |> post("/users/#{user.nickname}/outbox", activity)
1282 |> json_response(400)
1284 assert result == "Note is over the character limit"
1288 describe "/relay/followers" do
1289 test "it returns relay followers", %{conn: conn} do
1290 relay_actor = Relay.get_actor()
1291 user = insert(:user)
1292 User.follow(user, relay_actor)
1296 |> get("/relay/followers")
1297 |> json_response(200)
1299 assert result["first"]["orderedItems"] == [user.ap_id]
1302 test "on non-federating instance, it returns 404", %{conn: conn} do
1303 Config.put([:instance, :federating], false)
1304 user = insert(:user)
1307 |> assign(:user, user)
1308 |> get("/relay/followers")
1309 |> json_response(404)
1313 describe "/relay/following" do
1314 test "it returns relay following", %{conn: conn} do
1317 |> get("/relay/following")
1318 |> json_response(200)
1320 assert result["first"]["orderedItems"] == []
1323 test "on non-federating instance, it returns 404", %{conn: conn} do
1324 Config.put([:instance, :federating], false)
1325 user = insert(:user)
1328 |> assign(:user, user)
1329 |> get("/relay/following")
1330 |> json_response(404)
1334 describe "/users/:nickname/followers" do
1335 test "it returns the followers in a collection", %{conn: conn} do
1336 user = insert(:user)
1337 user_two = insert(:user)
1338 User.follow(user, user_two)
1342 |> assign(:user, user_two)
1343 |> get("/users/#{user_two.nickname}/followers")
1344 |> json_response(200)
1346 assert result["first"]["orderedItems"] == [user.ap_id]
1349 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1350 user = insert(:user)
1351 user_two = insert(:user, hide_followers: true)
1352 User.follow(user, user_two)
1356 |> assign(:user, user)
1357 |> get("/users/#{user_two.nickname}/followers")
1358 |> json_response(200)
1360 assert is_binary(result["first"])
1363 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1365 user = insert(:user)
1366 other_user = insert(:user, hide_followers: true)
1370 |> assign(:user, user)
1371 |> get("/users/#{other_user.nickname}/followers?page=1")
1373 assert result.status == 403
1374 assert result.resp_body == ""
1377 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1379 user = insert(:user, hide_followers: true)
1380 other_user = insert(:user)
1381 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1385 |> assign(:user, user)
1386 |> get("/users/#{user.nickname}/followers?page=1")
1387 |> json_response(200)
1389 assert result["totalItems"] == 1
1390 assert result["orderedItems"] == [other_user.ap_id]
1393 test "it works for more than 10 users", %{conn: conn} do
1394 user = insert(:user)
1396 Enum.each(1..15, fn _ ->
1397 other_user = insert(:user)
1398 User.follow(other_user, user)
1403 |> assign(:user, user)
1404 |> get("/users/#{user.nickname}/followers")
1405 |> json_response(200)
1407 assert length(result["first"]["orderedItems"]) == 10
1408 assert result["first"]["totalItems"] == 15
1409 assert result["totalItems"] == 15
1413 |> assign(:user, user)
1414 |> get("/users/#{user.nickname}/followers?page=2")
1415 |> json_response(200)
1417 assert length(result["orderedItems"]) == 5
1418 assert result["totalItems"] == 15
1421 test "does not require authentication", %{conn: conn} do
1422 user = insert(:user)
1425 |> get("/users/#{user.nickname}/followers")
1426 |> json_response(200)
1430 describe "/users/:nickname/following" do
1431 test "it returns the following in a collection", %{conn: conn} do
1432 user = insert(:user)
1433 user_two = insert(:user)
1434 User.follow(user, user_two)
1438 |> assign(:user, user)
1439 |> get("/users/#{user.nickname}/following")
1440 |> json_response(200)
1442 assert result["first"]["orderedItems"] == [user_two.ap_id]
1445 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1446 user = insert(:user)
1447 user_two = insert(:user, hide_follows: true)
1448 User.follow(user, user_two)
1452 |> assign(:user, user)
1453 |> get("/users/#{user_two.nickname}/following")
1454 |> json_response(200)
1456 assert is_binary(result["first"])
1459 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1461 user = insert(:user)
1462 user_two = insert(:user, hide_follows: true)
1466 |> assign(:user, user)
1467 |> get("/users/#{user_two.nickname}/following?page=1")
1469 assert result.status == 403
1470 assert result.resp_body == ""
1473 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1475 user = insert(:user, hide_follows: true)
1476 other_user = insert(:user)
1477 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1481 |> assign(:user, user)
1482 |> get("/users/#{user.nickname}/following?page=1")
1483 |> json_response(200)
1485 assert result["totalItems"] == 1
1486 assert result["orderedItems"] == [other_user.ap_id]
1489 test "it works for more than 10 users", %{conn: conn} do
1490 user = insert(:user)
1492 Enum.each(1..15, fn _ ->
1493 user = User.get_cached_by_id(user.id)
1494 other_user = insert(:user)
1495 User.follow(user, other_user)
1500 |> assign(:user, user)
1501 |> get("/users/#{user.nickname}/following")
1502 |> json_response(200)
1504 assert length(result["first"]["orderedItems"]) == 10
1505 assert result["first"]["totalItems"] == 15
1506 assert result["totalItems"] == 15
1510 |> assign(:user, user)
1511 |> get("/users/#{user.nickname}/following?page=2")
1512 |> json_response(200)
1514 assert length(result["orderedItems"]) == 5
1515 assert result["totalItems"] == 15
1518 test "does not require authentication", %{conn: conn} do
1519 user = insert(:user)
1522 |> get("/users/#{user.nickname}/following")
1523 |> json_response(200)
1527 describe "delivery tracking" do
1528 test "it tracks a signed object fetch", %{conn: conn} do
1529 user = insert(:user, local: false)
1530 activity = insert(:note_activity)
1531 object = Object.normalize(activity, fetch: false)
1533 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1536 |> put_req_header("accept", "application/activity+json")
1537 |> assign(:user, user)
1539 |> json_response(200)
1541 assert Delivery.get(object.id, user.id)
1544 test "it tracks a signed activity fetch", %{conn: conn} do
1545 user = insert(:user, local: false)
1546 activity = insert(:note_activity)
1547 object = Object.normalize(activity, fetch: false)
1549 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1552 |> put_req_header("accept", "application/activity+json")
1553 |> assign(:user, user)
1554 |> get(activity_path)
1555 |> json_response(200)
1557 assert Delivery.get(object.id, user.id)
1560 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1561 user = insert(:user, local: false)
1562 other_user = insert(:user, local: false)
1563 activity = insert(:note_activity)
1564 object = Object.normalize(activity, fetch: false)
1566 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1569 |> put_req_header("accept", "application/activity+json")
1570 |> assign(:user, user)
1572 |> json_response(200)
1575 |> put_req_header("accept", "application/activity+json")
1576 |> assign(:user, other_user)
1578 |> json_response(200)
1580 assert Delivery.get(object.id, user.id)
1581 assert Delivery.get(object.id, other_user.id)
1584 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1585 user = insert(:user, local: false)
1586 other_user = insert(:user, local: false)
1587 activity = insert(:note_activity)
1588 object = Object.normalize(activity, fetch: false)
1590 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1593 |> put_req_header("accept", "application/activity+json")
1594 |> assign(:user, user)
1595 |> get(activity_path)
1596 |> json_response(200)
1599 |> put_req_header("accept", "application/activity+json")
1600 |> assign(:user, other_user)
1601 |> get(activity_path)
1602 |> json_response(200)
1604 assert Delivery.get(object.id, user.id)
1605 assert Delivery.get(object.id, other_user.id)
1609 describe "Additional ActivityPub C2S endpoints" do
1610 test "GET /api/ap/whoami", %{conn: conn} do
1611 user = insert(:user)
1615 |> assign(:user, user)
1616 |> get("/api/ap/whoami")
1618 user = User.get_cached_by_id(user.id)
1620 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1623 |> get("/api/ap/whoami")
1624 |> json_response(403)
1627 setup do: clear_config([:media_proxy])
1628 setup do: clear_config([Pleroma.Upload])
1630 test "POST /api/ap/upload_media", %{conn: conn} do
1631 user = insert(:user)
1633 desc = "Description of the image"
1635 image = %Plug.Upload{
1636 content_type: "bad/content-type",
1637 path: Path.absname("test/fixtures/image.jpg"),
1638 filename: "an_image.png"
1643 |> assign(:user, user)
1644 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1645 |> json_response(:created)
1647 assert object["name"] == desc
1648 assert object["type"] == "Document"
1649 assert object["actor"] == user.ap_id
1650 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1651 assert is_binary(object_href)
1652 assert object_mediatype == "image/jpeg"
1653 assert String.ends_with?(object_href, ".jpg")
1655 activity_request = %{
1656 "@context" => "https://www.w3.org/ns/activitystreams",
1660 "content" => "AP C2S test, attachment",
1661 "attachment" => [object]
1663 "to" => "https://www.w3.org/ns/activitystreams#Public",
1669 |> assign(:user, user)
1670 |> post("/users/#{user.nickname}/outbox", activity_request)
1671 |> json_response(:created)
1673 assert activity_response["id"]
1674 assert activity_response["object"]
1675 assert activity_response["actor"] == user.ap_id
1677 assert %Object{data: %{"attachment" => [attachment]}} =
1678 Object.normalize(activity_response["object"], fetch: false)
1680 assert attachment["type"] == "Document"
1681 assert attachment["name"] == desc
1685 "href" => ^object_href,
1687 "mediaType" => ^object_mediatype
1689 ] = attachment["url"]
1691 # Fails if unauthenticated
1693 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1694 |> json_response(403)