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") |> 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)
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)
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"]
1028 describe "POST /users/:nickname/outbox (C2S)" do
1029 setup do: clear_config([:instance, :limit])
1034 "@context" => "https://www.w3.org/ns/activitystreams",
1036 "object" => %{"type" => "Note", "content" => "AP C2S test"},
1037 "to" => "https://www.w3.org/ns/activitystreams#Public",
1043 test "it rejects posts from other users / unauthenticated users", %{
1047 user = insert(:user)
1048 other_user = insert(:user)
1049 conn = put_req_header(conn, "content-type", "application/activity+json")
1052 |> post("/users/#{user.nickname}/outbox", activity)
1053 |> json_response(403)
1056 |> assign(:user, other_user)
1057 |> post("/users/#{user.nickname}/outbox", activity)
1058 |> json_response(403)
1061 test "it inserts an incoming create activity into the database", %{
1065 user = insert(:user)
1069 |> assign(:user, user)
1070 |> put_req_header("content-type", "application/activity+json")
1071 |> post("/users/#{user.nickname}/outbox", activity)
1072 |> json_response(201)
1074 assert Activity.get_by_ap_id(result["id"])
1075 assert result["object"]
1076 assert %Object{data: object} = Object.normalize(result["object"])
1077 assert object["content"] == activity["object"]["content"]
1080 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1081 user = insert(:user)
1085 |> put_in(["object", "type"], "Benis")
1089 |> assign(:user, user)
1090 |> put_req_header("content-type", "application/activity+json")
1091 |> post("/users/#{user.nickname}/outbox", activity)
1092 |> json_response(400)
1095 test "it inserts an incoming sensitive activity into the database", %{
1099 user = insert(:user)
1100 conn = assign(conn, :user, user)
1101 object = Map.put(activity["object"], "sensitive", true)
1102 activity = Map.put(activity, "object", object)
1106 |> put_req_header("content-type", "application/activity+json")
1107 |> post("/users/#{user.nickname}/outbox", activity)
1108 |> json_response(201)
1110 assert Activity.get_by_ap_id(response["id"])
1111 assert response["object"]
1112 assert %Object{data: response_object} = Object.normalize(response["object"])
1113 assert response_object["sensitive"] == true
1114 assert response_object["content"] == activity["object"]["content"]
1118 |> put_req_header("accept", "application/activity+json")
1119 |> get(response["id"])
1120 |> json_response(200)
1122 assert representation["object"]["sensitive"] == true
1125 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1126 user = insert(:user)
1127 activity = Map.put(activity, "type", "BadType")
1131 |> assign(:user, user)
1132 |> put_req_header("content-type", "application/activity+json")
1133 |> post("/users/#{user.nickname}/outbox", activity)
1135 assert json_response(conn, 400)
1138 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1139 note_activity = insert(:note_activity)
1140 note_object = Object.normalize(note_activity)
1141 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1146 id: note_object.data["id"]
1152 |> assign(:user, user)
1153 |> put_req_header("content-type", "application/activity+json")
1154 |> post("/users/#{user.nickname}/outbox", data)
1156 result = json_response(conn, 201)
1157 assert Activity.get_by_ap_id(result["id"])
1159 assert object = Object.get_by_ap_id(note_object.data["id"])
1160 assert object.data["type"] == "Tombstone"
1163 test "it rejects delete activity of object from other actor", %{conn: conn} do
1164 note_activity = insert(:note_activity)
1165 note_object = Object.normalize(note_activity)
1166 user = insert(:user)
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 assert json_response(conn, 400)
1184 test "it increases like count when receiving a like action", %{conn: conn} do
1185 note_activity = insert(:note_activity)
1186 note_object = Object.normalize(note_activity)
1187 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1192 id: note_object.data["id"]
1198 |> assign(:user, user)
1199 |> put_req_header("content-type", "application/activity+json")
1200 |> post("/users/#{user.nickname}/outbox", data)
1202 result = json_response(conn, 201)
1203 assert Activity.get_by_ap_id(result["id"])
1205 assert object = Object.get_by_ap_id(note_object.data["id"])
1206 assert object.data["like_count"] == 1
1209 test "it doesn't spreads faulty attributedTo or actor fields", %{
1213 reimu = insert(:user, nickname: "reimu")
1214 cirno = insert(:user, nickname: "cirno")
1221 |> put_in(["object", "actor"], reimu.ap_id)
1222 |> put_in(["object", "attributedTo"], reimu.ap_id)
1223 |> put_in(["actor"], reimu.ap_id)
1224 |> put_in(["attributedTo"], reimu.ap_id)
1228 |> assign(:user, cirno)
1229 |> put_req_header("content-type", "application/activity+json")
1230 |> post("/users/#{reimu.nickname}/outbox", activity)
1231 |> json_response(403)
1235 |> assign(:user, cirno)
1236 |> put_req_header("content-type", "application/activity+json")
1237 |> post("/users/#{cirno.nickname}/outbox", activity)
1238 |> json_response(201)
1240 assert cirno_outbox["attributedTo"] == nil
1241 assert cirno_outbox["actor"] == cirno.ap_id
1243 assert cirno_object = Object.normalize(cirno_outbox["object"])
1244 assert cirno_object.data["actor"] == cirno.ap_id
1245 assert cirno_object.data["attributedTo"] == cirno.ap_id
1248 test "Character limitation", %{conn: conn, activity: activity} do
1249 Pleroma.Config.put([:instance, :limit], 5)
1250 user = insert(:user)
1254 |> assign(:user, user)
1255 |> put_req_header("content-type", "application/activity+json")
1256 |> post("/users/#{user.nickname}/outbox", activity)
1257 |> json_response(400)
1259 assert result == "Note is over the character limit"
1263 describe "/relay/followers" do
1264 test "it returns relay followers", %{conn: conn} do
1265 relay_actor = Relay.get_actor()
1266 user = insert(:user)
1267 User.follow(user, relay_actor)
1271 |> get("/relay/followers")
1272 |> json_response(200)
1274 assert result["first"]["orderedItems"] == [user.ap_id]
1277 test "on non-federating instance, it returns 404", %{conn: conn} do
1278 Config.put([:instance, :federating], false)
1279 user = insert(:user)
1282 |> assign(:user, user)
1283 |> get("/relay/followers")
1284 |> json_response(404)
1288 describe "/relay/following" do
1289 test "it returns relay following", %{conn: conn} do
1292 |> get("/relay/following")
1293 |> json_response(200)
1295 assert result["first"]["orderedItems"] == []
1298 test "on non-federating instance, it returns 404", %{conn: conn} do
1299 Config.put([:instance, :federating], false)
1300 user = insert(:user)
1303 |> assign(:user, user)
1304 |> get("/relay/following")
1305 |> json_response(404)
1309 describe "/users/:nickname/followers" do
1310 test "it returns the followers in a collection", %{conn: conn} do
1311 user = insert(:user)
1312 user_two = insert(:user)
1313 User.follow(user, user_two)
1317 |> assign(:user, user_two)
1318 |> get("/users/#{user_two.nickname}/followers")
1319 |> json_response(200)
1321 assert result["first"]["orderedItems"] == [user.ap_id]
1324 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1325 user = insert(:user)
1326 user_two = insert(:user, hide_followers: true)
1327 User.follow(user, user_two)
1331 |> assign(:user, user)
1332 |> get("/users/#{user_two.nickname}/followers")
1333 |> json_response(200)
1335 assert is_binary(result["first"])
1338 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1340 user = insert(:user)
1341 other_user = insert(:user, hide_followers: true)
1345 |> assign(:user, user)
1346 |> get("/users/#{other_user.nickname}/followers?page=1")
1348 assert result.status == 403
1349 assert result.resp_body == ""
1352 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1354 user = insert(:user, hide_followers: true)
1355 other_user = insert(:user)
1356 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1360 |> assign(:user, user)
1361 |> get("/users/#{user.nickname}/followers?page=1")
1362 |> json_response(200)
1364 assert result["totalItems"] == 1
1365 assert result["orderedItems"] == [other_user.ap_id]
1368 test "it works for more than 10 users", %{conn: conn} do
1369 user = insert(:user)
1371 Enum.each(1..15, fn _ ->
1372 other_user = insert(:user)
1373 User.follow(other_user, user)
1378 |> assign(:user, user)
1379 |> get("/users/#{user.nickname}/followers")
1380 |> json_response(200)
1382 assert length(result["first"]["orderedItems"]) == 10
1383 assert result["first"]["totalItems"] == 15
1384 assert result["totalItems"] == 15
1388 |> assign(:user, user)
1389 |> get("/users/#{user.nickname}/followers?page=2")
1390 |> json_response(200)
1392 assert length(result["orderedItems"]) == 5
1393 assert result["totalItems"] == 15
1396 test "does not require authentication", %{conn: conn} do
1397 user = insert(:user)
1400 |> get("/users/#{user.nickname}/followers")
1401 |> json_response(200)
1405 describe "/users/:nickname/following" do
1406 test "it returns the following in a collection", %{conn: conn} do
1407 user = insert(:user)
1408 user_two = insert(:user)
1409 User.follow(user, user_two)
1413 |> assign(:user, user)
1414 |> get("/users/#{user.nickname}/following")
1415 |> json_response(200)
1417 assert result["first"]["orderedItems"] == [user_two.ap_id]
1420 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1421 user = insert(:user)
1422 user_two = insert(:user, hide_follows: true)
1423 User.follow(user, user_two)
1427 |> assign(:user, user)
1428 |> get("/users/#{user_two.nickname}/following")
1429 |> json_response(200)
1431 assert is_binary(result["first"])
1434 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1436 user = insert(:user)
1437 user_two = insert(:user, hide_follows: true)
1441 |> assign(:user, user)
1442 |> get("/users/#{user_two.nickname}/following?page=1")
1444 assert result.status == 403
1445 assert result.resp_body == ""
1448 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1450 user = insert(:user, hide_follows: true)
1451 other_user = insert(:user)
1452 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1456 |> assign(:user, user)
1457 |> get("/users/#{user.nickname}/following?page=1")
1458 |> json_response(200)
1460 assert result["totalItems"] == 1
1461 assert result["orderedItems"] == [other_user.ap_id]
1464 test "it works for more than 10 users", %{conn: conn} do
1465 user = insert(:user)
1467 Enum.each(1..15, fn _ ->
1468 user = User.get_cached_by_id(user.id)
1469 other_user = insert(:user)
1470 User.follow(user, other_user)
1475 |> assign(:user, user)
1476 |> get("/users/#{user.nickname}/following")
1477 |> json_response(200)
1479 assert length(result["first"]["orderedItems"]) == 10
1480 assert result["first"]["totalItems"] == 15
1481 assert result["totalItems"] == 15
1485 |> assign(:user, user)
1486 |> get("/users/#{user.nickname}/following?page=2")
1487 |> json_response(200)
1489 assert length(result["orderedItems"]) == 5
1490 assert result["totalItems"] == 15
1493 test "does not require authentication", %{conn: conn} do
1494 user = insert(:user)
1497 |> get("/users/#{user.nickname}/following")
1498 |> json_response(200)
1502 describe "delivery tracking" do
1503 test "it tracks a signed object fetch", %{conn: conn} do
1504 user = insert(:user, local: false)
1505 activity = insert(:note_activity)
1506 object = Object.normalize(activity)
1508 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1511 |> put_req_header("accept", "application/activity+json")
1512 |> assign(:user, user)
1514 |> json_response(200)
1516 assert Delivery.get(object.id, user.id)
1519 test "it tracks a signed activity fetch", %{conn: conn} do
1520 user = insert(:user, local: false)
1521 activity = insert(:note_activity)
1522 object = Object.normalize(activity)
1524 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1527 |> put_req_header("accept", "application/activity+json")
1528 |> assign(:user, user)
1529 |> get(activity_path)
1530 |> json_response(200)
1532 assert Delivery.get(object.id, user.id)
1535 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1536 user = insert(:user, local: false)
1537 other_user = insert(:user, local: false)
1538 activity = insert(:note_activity)
1539 object = Object.normalize(activity)
1541 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1544 |> put_req_header("accept", "application/activity+json")
1545 |> assign(:user, user)
1547 |> json_response(200)
1550 |> put_req_header("accept", "application/activity+json")
1551 |> assign(:user, other_user)
1553 |> json_response(200)
1555 assert Delivery.get(object.id, user.id)
1556 assert Delivery.get(object.id, other_user.id)
1559 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1560 user = insert(:user, local: false)
1561 other_user = insert(:user, local: false)
1562 activity = insert(:note_activity)
1563 object = Object.normalize(activity)
1565 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1568 |> put_req_header("accept", "application/activity+json")
1569 |> assign(:user, user)
1570 |> get(activity_path)
1571 |> json_response(200)
1574 |> put_req_header("accept", "application/activity+json")
1575 |> assign(:user, other_user)
1576 |> get(activity_path)
1577 |> json_response(200)
1579 assert Delivery.get(object.id, user.id)
1580 assert Delivery.get(object.id, other_user.id)
1584 describe "Additional ActivityPub C2S endpoints" do
1585 test "GET /api/ap/whoami", %{conn: conn} do
1586 user = insert(:user)
1590 |> assign(:user, user)
1591 |> get("/api/ap/whoami")
1593 user = User.get_cached_by_id(user.id)
1595 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1598 |> get("/api/ap/whoami")
1599 |> json_response(403)
1602 setup do: clear_config([:media_proxy])
1603 setup do: clear_config([Pleroma.Upload])
1605 test "POST /api/ap/upload_media", %{conn: conn} do
1606 user = insert(:user)
1608 desc = "Description of the image"
1610 image = %Plug.Upload{
1611 content_type: "bad/content-type",
1612 path: Path.absname("test/fixtures/image.jpg"),
1613 filename: "an_image.png"
1618 |> assign(:user, user)
1619 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1620 |> json_response(:created)
1622 assert object["name"] == desc
1623 assert object["type"] == "Document"
1624 assert object["actor"] == user.ap_id
1625 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1626 assert is_binary(object_href)
1627 assert object_mediatype == "image/jpeg"
1628 assert String.ends_with?(object_href, ".jpg")
1630 activity_request = %{
1631 "@context" => "https://www.w3.org/ns/activitystreams",
1635 "content" => "AP C2S test, attachment",
1636 "attachment" => [object]
1638 "to" => "https://www.w3.org/ns/activitystreams#Public",
1644 |> assign(:user, user)
1645 |> post("/users/#{user.nickname}/outbox", activity_request)
1646 |> json_response(:created)
1648 assert activity_response["id"]
1649 assert activity_response["object"]
1650 assert activity_response["actor"] == user.ap_id
1652 assert %Object{data: %{"attachment" => [attachment]}} =
1653 Object.normalize(activity_response["object"])
1655 assert attachment["type"] == "Document"
1656 assert attachment["name"] == desc
1660 "href" => ^object_href,
1662 "mediaType" => ^object_mediatype
1664 ] = attachment["url"]
1666 # Fails if unauthenticated
1668 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1669 |> json_response(403)