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
10 alias Pleroma.Delivery
11 alias Pleroma.Instances
13 alias Pleroma.Tests.ObanHelpers
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.ObjectView
17 alias Pleroma.Web.ActivityPub.Relay
18 alias Pleroma.Web.ActivityPub.UserView
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.Endpoint
22 alias Pleroma.Workers.ReceiverWorker
24 import Pleroma.Factory
26 require Pleroma.Constants
29 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
33 setup do: clear_config([:instance, :federating], true)
36 setup do: clear_config([:instance, :allow_relay])
38 test "with the relay active, it returns the relay user", %{conn: conn} do
41 |> get(activity_pub_path(conn, :relay))
44 assert res["id"] =~ "/relay"
47 test "with the relay disabled, it returns 404", %{conn: conn} do
48 clear_config([:instance, :allow_relay], false)
51 |> get(activity_pub_path(conn, :relay))
55 test "on non-federating instance, it returns 404", %{conn: conn} do
56 clear_config([:instance, :federating], false)
60 |> assign(:user, user)
61 |> get(activity_pub_path(conn, :relay))
66 describe "/internal/fetch" do
67 test "it returns the internal fetch user", %{conn: conn} do
70 |> get(activity_pub_path(conn, :internal_fetch))
73 assert res["id"] =~ "/fetch"
76 test "on non-federating instance, it returns 404", %{conn: conn} do
77 clear_config([:instance, :federating], false)
81 |> assign(:user, user)
82 |> get(activity_pub_path(conn, :internal_fetch))
87 describe "/users/:nickname" do
88 test "it returns a json representation of the user with accept application/json", %{
95 |> put_req_header("accept", "application/json")
96 |> get("/users/#{user.nickname}")
98 user = User.get_cached_by_id(user.id)
100 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
103 test "it returns a json representation of the user with accept application/activity+json", %{
110 |> put_req_header("accept", "application/activity+json")
111 |> get("/users/#{user.nickname}")
113 user = User.get_cached_by_id(user.id)
115 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
118 test "it returns a json representation of the user with accept application/ld+json", %{
127 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
129 |> get("/users/#{user.nickname}")
131 user = User.get_cached_by_id(user.id)
133 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
136 test "it returns 404 for remote users", %{
139 user = insert(:user, local: false, nickname: "remoteuser@example.com")
143 |> put_req_header("accept", "application/json")
144 |> get("/users/#{user.nickname}.json")
146 assert json_response(conn, 404)
149 test "it returns error when user is not found", %{conn: conn} do
152 |> put_req_header("accept", "application/json")
153 |> get("/users/jimm")
154 |> json_response(404)
156 assert response == "Not found"
160 describe "mastodon compatibility routes" do
161 test "it returns a json representation of the object with accept application/json", %{
168 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
169 "actor" => Endpoint.url() <> "/users/raymoo",
170 "to" => [Pleroma.Constants.as_public()]
176 |> put_req_header("accept", "application/json")
177 |> get("/users/raymoo/statuses/999999999")
179 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
182 test "it returns a json representation of the activity with accept application/json", %{
189 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
190 "actor" => Endpoint.url() <> "/users/raymoo",
191 "to" => [Pleroma.Constants.as_public()]
197 "id" => object.data["id"] <> "/activity",
199 "object" => object.data["id"],
200 "actor" => object.data["actor"],
201 "to" => object.data["to"]
203 |> ActivityPub.persist(local: true)
207 |> put_req_header("accept", "application/json")
208 |> get("/users/raymoo/statuses/999999999/activity")
210 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
214 describe "/objects/:uuid" do
215 test "it doesn't return a local-only object", %{conn: conn} do
217 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
219 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
221 object = Object.normalize(post, fetch: false)
222 uuid = String.split(object.data["id"], "/") |> List.last()
226 |> put_req_header("accept", "application/json")
227 |> get("/objects/#{uuid}")
229 assert json_response(conn, 404)
232 test "returns local-only objects when authenticated", %{conn: conn} do
234 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
236 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
238 object = Object.normalize(post, fetch: false)
239 uuid = String.split(object.data["id"], "/") |> List.last()
243 |> assign(:user, user)
244 |> put_req_header("accept", "application/activity+json")
245 |> get("/objects/#{uuid}")
247 assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
250 test "it returns a json representation of the object with accept application/json", %{
254 uuid = String.split(note.data["id"], "/") |> List.last()
258 |> put_req_header("accept", "application/json")
259 |> get("/objects/#{uuid}")
261 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
264 test "it returns a json representation of the object with accept application/activity+json",
267 uuid = String.split(note.data["id"], "/") |> List.last()
271 |> put_req_header("accept", "application/activity+json")
272 |> get("/objects/#{uuid}")
274 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
277 test "it returns a json representation of the object with accept application/ld+json", %{
281 uuid = String.split(note.data["id"], "/") |> List.last()
287 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
289 |> get("/objects/#{uuid}")
291 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
294 test "it returns 404 for non-public messages", %{conn: conn} do
295 note = insert(:direct_note)
296 uuid = String.split(note.data["id"], "/") |> List.last()
300 |> put_req_header("accept", "application/activity+json")
301 |> get("/objects/#{uuid}")
303 assert json_response(conn, 404)
306 test "returns visible non-public messages when authenticated", %{conn: conn} do
307 note = insert(:direct_note)
308 uuid = String.split(note.data["id"], "/") |> List.last()
309 user = User.get_by_ap_id(note.data["actor"])
310 marisa = insert(:user)
313 |> assign(:user, marisa)
314 |> put_req_header("accept", "application/activity+json")
315 |> get("/objects/#{uuid}")
316 |> json_response(404)
320 |> assign(:user, user)
321 |> put_req_header("accept", "application/activity+json")
322 |> get("/objects/#{uuid}")
323 |> json_response(200)
325 assert response == ObjectView.render("object.json", %{object: note})
328 test "it returns 404 for tombstone objects", %{conn: conn} do
329 tombstone = insert(:tombstone)
330 uuid = String.split(tombstone.data["id"], "/") |> List.last()
334 |> put_req_header("accept", "application/activity+json")
335 |> get("/objects/#{uuid}")
337 assert json_response(conn, 404)
340 test "it caches a response", %{conn: conn} do
342 uuid = String.split(note.data["id"], "/") |> List.last()
346 |> put_req_header("accept", "application/activity+json")
347 |> get("/objects/#{uuid}")
349 assert json_response(conn1, :ok)
350 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
354 |> put_req_header("accept", "application/activity+json")
355 |> get("/objects/#{uuid}")
357 assert json_response(conn1, :ok) == json_response(conn2, :ok)
358 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
361 test "cached purged after object deletion", %{conn: conn} do
363 uuid = String.split(note.data["id"], "/") |> List.last()
367 |> put_req_header("accept", "application/activity+json")
368 |> get("/objects/#{uuid}")
370 assert json_response(conn1, :ok)
371 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
377 |> put_req_header("accept", "application/activity+json")
378 |> get("/objects/#{uuid}")
380 assert "Not found" == json_response(conn2, :not_found)
384 describe "/activities/:uuid" do
385 test "it doesn't return a local-only activity", %{conn: conn} do
387 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
389 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
391 uuid = String.split(post.data["id"], "/") |> List.last()
395 |> put_req_header("accept", "application/json")
396 |> get("/activities/#{uuid}")
398 assert json_response(conn, 404)
401 test "returns local-only activities when authenticated", %{conn: conn} do
403 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
405 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
407 uuid = String.split(post.data["id"], "/") |> List.last()
411 |> assign(:user, user)
412 |> put_req_header("accept", "application/activity+json")
413 |> get("/activities/#{uuid}")
415 assert json_response(response, 200) == ObjectView.render("object.json", %{object: post})
418 test "it returns a json representation of the activity", %{conn: conn} do
419 activity = insert(:note_activity)
420 uuid = String.split(activity.data["id"], "/") |> List.last()
424 |> put_req_header("accept", "application/activity+json")
425 |> get("/activities/#{uuid}")
427 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
430 test "it returns 404 for non-public activities", %{conn: conn} do
431 activity = insert(:direct_note_activity)
432 uuid = String.split(activity.data["id"], "/") |> List.last()
436 |> put_req_header("accept", "application/activity+json")
437 |> get("/activities/#{uuid}")
439 assert json_response(conn, 404)
442 test "returns visible non-public messages when authenticated", %{conn: conn} do
443 note = insert(:direct_note_activity)
444 uuid = String.split(note.data["id"], "/") |> List.last()
445 user = User.get_by_ap_id(note.data["actor"])
446 marisa = insert(:user)
449 |> assign(:user, marisa)
450 |> put_req_header("accept", "application/activity+json")
451 |> get("/activities/#{uuid}")
452 |> json_response(404)
456 |> assign(:user, user)
457 |> put_req_header("accept", "application/activity+json")
458 |> get("/activities/#{uuid}")
459 |> json_response(200)
461 assert response == ObjectView.render("object.json", %{object: note})
464 test "it caches a response", %{conn: conn} do
465 activity = insert(:note_activity)
466 uuid = String.split(activity.data["id"], "/") |> List.last()
470 |> put_req_header("accept", "application/activity+json")
471 |> get("/activities/#{uuid}")
473 assert json_response(conn1, :ok)
474 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
478 |> put_req_header("accept", "application/activity+json")
479 |> get("/activities/#{uuid}")
481 assert json_response(conn1, :ok) == json_response(conn2, :ok)
482 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
485 test "cached purged after activity deletion", %{conn: conn} do
487 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
489 uuid = String.split(activity.data["id"], "/") |> List.last()
493 |> put_req_header("accept", "application/activity+json")
494 |> get("/activities/#{uuid}")
496 assert json_response(conn1, :ok)
497 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
499 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
503 |> put_req_header("accept", "application/activity+json")
504 |> get("/activities/#{uuid}")
506 assert "Not found" == json_response(conn2, :not_found)
511 test "it inserts an incoming activity into the database", %{conn: conn} do
512 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
516 |> assign(:valid_signature, true)
517 |> put_req_header("content-type", "application/activity+json")
518 |> post("/inbox", data)
520 assert "ok" == json_response(conn, 200)
522 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
523 assert Activity.get_by_ap_id(data["id"])
526 @tag capture_log: true
527 test "it inserts an incoming activity into the database" <>
528 "even if we can't fetch the user but have it in our db",
532 ap_id: "https://mastodon.example.org/users/raymoo",
535 last_refreshed_at: nil
539 File.read!("test/fixtures/mastodon-post-activity.json")
541 |> Map.put("actor", user.ap_id)
542 |> put_in(["object", "attributedTo"], user.ap_id)
546 |> assign(:valid_signature, true)
547 |> put_req_header("content-type", "application/activity+json")
548 |> post("/inbox", data)
550 assert "ok" == json_response(conn, 200)
552 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
553 assert Activity.get_by_ap_id(data["id"])
556 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
557 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
559 sender_url = data["actor"]
560 Instances.set_consistently_unreachable(sender_url)
561 refute Instances.reachable?(sender_url)
565 |> assign(:valid_signature, true)
566 |> put_req_header("content-type", "application/activity+json")
567 |> post("/inbox", data)
569 assert "ok" == json_response(conn, 200)
570 assert Instances.reachable?(sender_url)
573 test "accept follow activity", %{conn: conn} do
574 clear_config([:instance, :federating], true)
575 relay = Relay.get_actor()
577 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
579 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
580 relay = refresh_record(relay)
583 File.read!("test/fixtures/relay/accept-follow.json")
584 |> String.replace("{{ap_id}}", relay.ap_id)
585 |> String.replace("{{activity_id}}", activity.data["id"])
589 |> assign(:valid_signature, true)
590 |> put_req_header("content-type", "application/activity+json")
591 |> post("/inbox", accept)
592 |> json_response(200)
594 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
596 assert Pleroma.FollowingRelationship.following?(
601 Mix.shell(Mix.Shell.Process)
604 Mix.shell(Mix.Shell.IO)
607 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
608 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
611 @tag capture_log: true
612 test "without valid signature, " <>
613 "it only accepts Create activities and requires enabled federation",
615 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
616 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
618 conn = put_req_header(conn, "content-type", "application/activity+json")
620 clear_config([:instance, :federating], false)
623 |> post("/inbox", data)
624 |> json_response(403)
627 |> post("/inbox", non_create_data)
628 |> json_response(403)
630 clear_config([:instance, :federating], true)
632 ret_conn = post(conn, "/inbox", data)
633 assert "ok" == json_response(ret_conn, 200)
636 |> post("/inbox", non_create_data)
637 |> json_response(400)
641 describe "/users/:nickname/inbox" do
644 File.read!("test/fixtures/mastodon-post-activity.json")
650 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
652 data = Map.put(data, "bcc", [user.ap_id])
656 |> assign(:valid_signature, true)
657 |> put_req_header("content-type", "application/activity+json")
658 |> post("/users/#{user.nickname}/inbox", data)
660 assert "ok" == json_response(conn, 200)
661 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
662 assert Activity.get_by_ap_id(data["id"])
665 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
669 Map.put(data, "to", user.ap_id)
674 |> assign(:valid_signature, true)
675 |> put_req_header("content-type", "application/activity+json")
676 |> post("/users/#{user.nickname}/inbox", data)
678 assert "ok" == json_response(conn, 200)
679 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
680 assert Activity.get_by_ap_id(data["id"])
683 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
687 Map.put(data, "cc", user.ap_id)
692 |> assign(:valid_signature, true)
693 |> put_req_header("content-type", "application/activity+json")
694 |> post("/users/#{user.nickname}/inbox", data)
696 assert "ok" == json_response(conn, 200)
697 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
698 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
699 assert user.ap_id in activity.recipients
702 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
706 Map.put(data, "bcc", user.ap_id)
712 |> assign(:valid_signature, true)
713 |> put_req_header("content-type", "application/activity+json")
714 |> post("/users/#{user.nickname}/inbox", data)
716 assert "ok" == json_response(conn, 200)
717 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
718 assert Activity.get_by_ap_id(data["id"])
721 test "it accepts announces with to as string instead of array", %{conn: conn} do
724 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
725 announcer = insert(:user, local: false)
728 "@context" => "https://www.w3.org/ns/activitystreams",
729 "actor" => announcer.ap_id,
730 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
731 "object" => post.data["object"],
732 "to" => "https://www.w3.org/ns/activitystreams#Public",
733 "cc" => [user.ap_id],
739 |> assign(:valid_signature, true)
740 |> put_req_header("content-type", "application/activity+json")
741 |> post("/users/#{user.nickname}/inbox", data)
743 assert "ok" == json_response(conn, 200)
744 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
745 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
746 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
749 test "it accepts messages from actors that are followed by the user", %{
753 recipient = insert(:user)
754 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
756 {:ok, recipient, actor} = User.follow(recipient, actor)
760 |> Map.put("attributedTo", actor.ap_id)
764 |> Map.put("actor", actor.ap_id)
765 |> Map.put("object", object)
769 |> assign(:valid_signature, true)
770 |> put_req_header("content-type", "application/activity+json")
771 |> post("/users/#{recipient.nickname}/inbox", data)
773 assert "ok" == json_response(conn, 200)
774 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
775 assert Activity.get_by_ap_id(data["id"])
778 test "it rejects reads from other users", %{conn: conn} do
780 other_user = insert(:user)
784 |> assign(:user, other_user)
785 |> put_req_header("accept", "application/activity+json")
786 |> get("/users/#{user.nickname}/inbox")
788 assert json_response(conn, 403)
791 test "it returns a note activity in a collection", %{conn: conn} do
792 note_activity = insert(:direct_note_activity)
793 note_object = Object.normalize(note_activity, fetch: false)
794 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
798 |> assign(:user, user)
799 |> put_req_header("accept", "application/activity+json")
800 |> get("/users/#{user.nickname}/inbox?page=true")
802 assert response(conn, 200) =~ note_object.data["content"]
805 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
807 data = Map.put(data, "bcc", [user.ap_id])
809 sender_host = URI.parse(data["actor"]).host
810 Instances.set_consistently_unreachable(sender_host)
811 refute Instances.reachable?(sender_host)
815 |> assign(:valid_signature, true)
816 |> put_req_header("content-type", "application/activity+json")
817 |> post("/users/#{user.nickname}/inbox", data)
819 assert "ok" == json_response(conn, 200)
820 assert Instances.reachable?(sender_host)
823 @tag capture_log: true
824 test "it removes all follower collections but actor's", %{conn: conn} do
825 [actor, recipient] = insert_pair(:user)
829 recipient.follower_address,
830 "https://www.w3.org/ns/activitystreams#Public"
833 cc = [recipient.follower_address, actor.follower_address]
836 "@context" => ["https://www.w3.org/ns/activitystreams"],
838 "id" => Utils.generate_activity_id(),
841 "actor" => actor.ap_id,
846 "content" => "It's a note",
847 "attributedTo" => actor.ap_id,
848 "id" => Utils.generate_object_id()
853 |> assign(:valid_signature, true)
854 |> put_req_header("content-type", "application/activity+json")
855 |> post("/users/#{recipient.nickname}/inbox", data)
856 |> json_response(200)
858 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
860 assert activity = Activity.get_by_ap_id(data["id"])
863 assert actor.follower_address in activity.recipients
864 assert actor.follower_address in activity.data["cc"]
866 refute recipient.follower_address in activity.recipients
867 refute recipient.follower_address in activity.data["cc"]
868 refute recipient.follower_address in activity.data["to"]
871 test "it requires authentication", %{conn: conn} do
873 conn = put_req_header(conn, "accept", "application/activity+json")
875 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
876 assert json_response(ret_conn, 403)
880 |> assign(:user, user)
881 |> get("/users/#{user.nickname}/inbox")
883 assert json_response(ret_conn, 200)
886 @tag capture_log: true
887 test "forwarded report", %{conn: conn} do
888 admin = insert(:user, is_admin: true)
889 actor = insert(:user, local: false)
890 remote_domain = URI.parse(actor.ap_id).host
891 reported_user = insert(:user)
893 note = insert(:note_activity, user: reported_user)
897 "https://www.w3.org/ns/activitystreams",
898 "https://#{remote_domain}/schemas/litepub-0.1.jsonld",
903 "actor" => actor.ap_id,
908 "context" => "context",
909 "id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
910 "nickname" => reported_user.nickname,
915 "actor_type" => "Person",
916 "approval_pending" => false,
918 "confirmation_pending" => false,
919 "deactivated" => false,
920 "display_name" => "test user",
921 "id" => reported_user.id,
923 "nickname" => reported_user.nickname,
924 "registration_reason" => nil,
930 "url" => reported_user.ap_id
933 "id" => note.data["id"],
934 "published" => note.data["published"],
938 "published" => note.data["published"],
945 |> assign(:valid_signature, true)
946 |> put_req_header("content-type", "application/activity+json")
947 |> post("/users/#{reported_user.nickname}/inbox", data)
948 |> json_response(200)
950 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
952 assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
954 ObanHelpers.perform_all()
956 Swoosh.TestAssertions.assert_email_sent(
957 to: {admin.name, admin.email},
958 html_body: ~r/Reported Account:/i
962 @tag capture_log: true
963 test "forwarded report from mastodon", %{conn: conn} do
964 admin = insert(:user, is_admin: true)
965 actor = insert(:user, local: false)
966 remote_domain = URI.parse(actor.ap_id).host
967 remote_actor = "https://#{remote_domain}/actor"
968 [reported_user, another] = insert_list(2, :user)
970 note = insert(:note_activity, user: reported_user)
972 Pleroma.Web.CommonAPI.favorite(another, note.id)
975 "test/fixtures/mastodon/application_actor.json"
977 |> String.replace("{{DOMAIN}}", remote_domain)
979 Tesla.Mock.mock(fn %{url: ^remote_actor} ->
982 body: mock_json_body,
983 headers: [{"content-type", "application/activity+json"}]
988 "@context" => "https://www.w3.org/ns/activitystreams",
989 "actor" => remote_actor,
990 "content" => "test report",
991 "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
992 "nickname" => reported_user.nickname,
1001 |> assign(:valid_signature, true)
1002 |> put_req_header("content-type", "application/activity+json")
1003 |> post("/users/#{reported_user.nickname}/inbox", data)
1004 |> json_response(200)
1006 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
1008 flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
1009 reported_user_ap_id = reported_user.ap_id
1011 [^reported_user_ap_id, flag_data] = flag_activity.data["object"]
1013 Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
1014 ObanHelpers.perform_all()
1016 Swoosh.TestAssertions.assert_email_sent(
1017 to: {admin.name, admin.email},
1018 html_body: ~r/#{note.data["object"]}/i
1023 describe "GET /users/:nickname/outbox" do
1024 test "it paginates correctly", %{conn: conn} do
1025 user = insert(:user)
1026 conn = assign(conn, :user, user)
1027 outbox_endpoint = user.ap_id <> "/outbox"
1031 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
1037 |> put_req_header("accept", "application/activity+json")
1038 |> get(outbox_endpoint <> "?page=true")
1039 |> json_response(200)
1041 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
1042 assert length(result["orderedItems"]) == 20
1043 assert length(result_ids) == 20
1044 assert result["next"]
1045 assert String.starts_with?(result["next"], outbox_endpoint)
1049 |> put_req_header("accept", "application/activity+json")
1050 |> get(result["next"])
1051 |> json_response(200)
1053 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
1054 assert length(result_next["orderedItems"]) == 6
1055 assert length(result_next_ids) == 6
1056 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
1057 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
1058 assert String.starts_with?(result["id"], outbox_endpoint)
1062 |> put_req_header("accept", "application/activity+json")
1063 |> get(result_next["id"])
1064 |> json_response(200)
1066 assert result_next == result_next_again
1069 test "it returns 200 even if there're no activities", %{conn: conn} do
1070 user = insert(:user)
1071 outbox_endpoint = user.ap_id <> "/outbox"
1075 |> assign(:user, user)
1076 |> put_req_header("accept", "application/activity+json")
1077 |> get(outbox_endpoint)
1079 result = json_response(conn, 200)
1080 assert outbox_endpoint == result["id"]
1083 test "it returns a note activity in a collection", %{conn: conn} do
1084 note_activity = insert(:note_activity)
1085 note_object = Object.normalize(note_activity, fetch: false)
1086 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1090 |> assign(:user, user)
1091 |> put_req_header("accept", "application/activity+json")
1092 |> get("/users/#{user.nickname}/outbox?page=true")
1094 assert response(conn, 200) =~ note_object.data["content"]
1097 test "it returns an announce activity in a collection", %{conn: conn} do
1098 announce_activity = insert(:announce_activity)
1099 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
1103 |> assign(:user, user)
1104 |> put_req_header("accept", "application/activity+json")
1105 |> get("/users/#{user.nickname}/outbox?page=true")
1107 assert response(conn, 200) =~ announce_activity.data["object"]
1110 test "It returns poll Answers when authenticated", %{conn: conn} do
1111 poller = insert(:user)
1112 voter = insert(:user)
1115 CommonAPI.post(poller, %{
1117 poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
1120 assert question = Object.normalize(activity, fetch: false)
1122 {:ok, [activity], _object} = CommonAPI.vote(voter, question, [1])
1126 |> assign(:user, voter)
1127 |> put_req_header("accept", "application/activity+json")
1128 |> get(voter.ap_id <> "/outbox?page=true")
1129 |> json_response(200)
1131 assert [answer_outbox] = outbox_get["orderedItems"]
1132 assert answer_outbox["id"] == activity.data["id"]
1136 describe "POST /users/:nickname/outbox (C2S)" do
1137 setup do: clear_config([:instance, :limit])
1142 "@context" => "https://www.w3.org/ns/activitystreams",
1144 "object" => %{"type" => "Note", "content" => "AP C2S test"},
1145 "to" => "https://www.w3.org/ns/activitystreams#Public",
1151 test "it rejects posts from other users / unauthenticated users", %{
1155 user = insert(:user)
1156 other_user = insert(:user)
1157 conn = put_req_header(conn, "content-type", "application/activity+json")
1160 |> post("/users/#{user.nickname}/outbox", activity)
1161 |> json_response(403)
1164 |> assign(:user, other_user)
1165 |> post("/users/#{user.nickname}/outbox", activity)
1166 |> json_response(403)
1169 test "it inserts an incoming create activity into the database", %{
1173 user = insert(:user)
1177 |> assign(:user, user)
1178 |> put_req_header("content-type", "application/activity+json")
1179 |> post("/users/#{user.nickname}/outbox", activity)
1180 |> json_response(201)
1182 assert Activity.get_by_ap_id(result["id"])
1183 assert result["object"]
1184 assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
1185 assert object["content"] == activity["object"]["content"]
1188 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1189 user = insert(:user)
1193 |> put_in(["object", "type"], "Benis")
1197 |> assign(:user, user)
1198 |> put_req_header("content-type", "application/activity+json")
1199 |> post("/users/#{user.nickname}/outbox", activity)
1200 |> json_response(400)
1203 test "it inserts an incoming sensitive activity into the database", %{
1207 user = insert(:user)
1208 conn = assign(conn, :user, user)
1209 object = Map.put(activity["object"], "sensitive", true)
1210 activity = Map.put(activity, "object", object)
1214 |> put_req_header("content-type", "application/activity+json")
1215 |> post("/users/#{user.nickname}/outbox", activity)
1216 |> json_response(201)
1218 assert Activity.get_by_ap_id(response["id"])
1219 assert response["object"]
1220 assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false)
1221 assert response_object["sensitive"] == true
1222 assert response_object["content"] == activity["object"]["content"]
1226 |> put_req_header("accept", "application/activity+json")
1227 |> get(response["id"])
1228 |> json_response(200)
1230 assert representation["object"]["sensitive"] == true
1233 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1234 user = insert(:user)
1235 activity = Map.put(activity, "type", "BadType")
1239 |> assign(:user, user)
1240 |> put_req_header("content-type", "application/activity+json")
1241 |> post("/users/#{user.nickname}/outbox", activity)
1243 assert json_response(conn, 400)
1246 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1247 note_activity = insert(:note_activity)
1248 note_object = Object.normalize(note_activity, fetch: false)
1249 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1254 id: note_object.data["id"]
1260 |> assign(:user, user)
1261 |> put_req_header("content-type", "application/activity+json")
1262 |> post("/users/#{user.nickname}/outbox", data)
1264 result = json_response(conn, 201)
1265 assert Activity.get_by_ap_id(result["id"])
1267 assert object = Object.get_by_ap_id(note_object.data["id"])
1268 assert object.data["type"] == "Tombstone"
1271 test "it rejects delete activity of object from other actor", %{conn: conn} do
1272 note_activity = insert(:note_activity)
1273 note_object = Object.normalize(note_activity, fetch: false)
1274 user = insert(:user)
1279 id: note_object.data["id"]
1285 |> assign(:user, user)
1286 |> put_req_header("content-type", "application/activity+json")
1287 |> post("/users/#{user.nickname}/outbox", data)
1289 assert json_response(conn, 400)
1292 test "it increases like count when receiving a like action", %{conn: conn} do
1293 note_activity = insert(:note_activity)
1294 note_object = Object.normalize(note_activity, fetch: false)
1295 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1300 id: note_object.data["id"]
1306 |> assign(:user, user)
1307 |> put_req_header("content-type", "application/activity+json")
1308 |> post("/users/#{user.nickname}/outbox", data)
1310 result = json_response(conn, 201)
1311 assert Activity.get_by_ap_id(result["id"])
1313 assert object = Object.get_by_ap_id(note_object.data["id"])
1314 assert object.data["like_count"] == 1
1317 test "it doesn't spreads faulty attributedTo or actor fields", %{
1321 reimu = insert(:user, nickname: "reimu")
1322 cirno = insert(:user, nickname: "cirno")
1329 |> put_in(["object", "actor"], reimu.ap_id)
1330 |> put_in(["object", "attributedTo"], reimu.ap_id)
1331 |> put_in(["actor"], reimu.ap_id)
1332 |> put_in(["attributedTo"], reimu.ap_id)
1336 |> assign(:user, cirno)
1337 |> put_req_header("content-type", "application/activity+json")
1338 |> post("/users/#{reimu.nickname}/outbox", activity)
1339 |> json_response(403)
1343 |> assign(:user, cirno)
1344 |> put_req_header("content-type", "application/activity+json")
1345 |> post("/users/#{cirno.nickname}/outbox", activity)
1346 |> json_response(201)
1348 assert cirno_outbox["attributedTo"] == nil
1349 assert cirno_outbox["actor"] == cirno.ap_id
1351 assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false)
1352 assert cirno_object.data["actor"] == cirno.ap_id
1353 assert cirno_object.data["attributedTo"] == cirno.ap_id
1356 test "Character limitation", %{conn: conn, activity: activity} do
1357 clear_config([:instance, :limit], 5)
1358 user = insert(:user)
1362 |> assign(:user, user)
1363 |> put_req_header("content-type", "application/activity+json")
1364 |> post("/users/#{user.nickname}/outbox", activity)
1365 |> json_response(400)
1367 assert result == "Note is over the character limit"
1371 describe "/relay/followers" do
1372 test "it returns relay followers", %{conn: conn} do
1373 relay_actor = Relay.get_actor()
1374 user = insert(:user)
1375 User.follow(user, relay_actor)
1379 |> get("/relay/followers")
1380 |> json_response(200)
1382 assert result["first"]["orderedItems"] == [user.ap_id]
1385 test "on non-federating instance, it returns 404", %{conn: conn} do
1386 clear_config([:instance, :federating], false)
1387 user = insert(:user)
1390 |> assign(:user, user)
1391 |> get("/relay/followers")
1392 |> json_response(404)
1396 describe "/relay/following" do
1397 test "it returns relay following", %{conn: conn} do
1400 |> get("/relay/following")
1401 |> json_response(200)
1403 assert result["first"]["orderedItems"] == []
1406 test "on non-federating instance, it returns 404", %{conn: conn} do
1407 clear_config([:instance, :federating], false)
1408 user = insert(:user)
1411 |> assign(:user, user)
1412 |> get("/relay/following")
1413 |> json_response(404)
1417 describe "/users/:nickname/followers" do
1418 test "it returns the followers in a collection", %{conn: conn} do
1419 user = insert(:user)
1420 user_two = insert(:user)
1421 User.follow(user, user_two)
1425 |> assign(:user, user_two)
1426 |> get("/users/#{user_two.nickname}/followers")
1427 |> json_response(200)
1429 assert result["first"]["orderedItems"] == [user.ap_id]
1432 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1433 user = insert(:user)
1434 user_two = insert(:user, hide_followers: true)
1435 User.follow(user, user_two)
1439 |> assign(:user, user)
1440 |> get("/users/#{user_two.nickname}/followers")
1441 |> json_response(200)
1443 assert is_binary(result["first"])
1446 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1448 user = insert(:user)
1449 other_user = insert(:user, hide_followers: true)
1453 |> assign(:user, user)
1454 |> get("/users/#{other_user.nickname}/followers?page=1")
1456 assert result.status == 403
1457 assert result.resp_body == ""
1460 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1462 user = insert(:user, hide_followers: true)
1463 other_user = insert(:user)
1464 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1468 |> assign(:user, user)
1469 |> get("/users/#{user.nickname}/followers?page=1")
1470 |> json_response(200)
1472 assert result["totalItems"] == 1
1473 assert result["orderedItems"] == [other_user.ap_id]
1476 test "it works for more than 10 users", %{conn: conn} do
1477 user = insert(:user)
1479 Enum.each(1..15, fn _ ->
1480 other_user = insert(:user)
1481 User.follow(other_user, user)
1486 |> assign(:user, user)
1487 |> get("/users/#{user.nickname}/followers")
1488 |> json_response(200)
1490 assert length(result["first"]["orderedItems"]) == 10
1491 assert result["first"]["totalItems"] == 15
1492 assert result["totalItems"] == 15
1496 |> assign(:user, user)
1497 |> get("/users/#{user.nickname}/followers?page=2")
1498 |> json_response(200)
1500 assert length(result["orderedItems"]) == 5
1501 assert result["totalItems"] == 15
1504 test "does not require authentication", %{conn: conn} do
1505 user = insert(:user)
1508 |> get("/users/#{user.nickname}/followers")
1509 |> json_response(200)
1513 describe "/users/:nickname/following" do
1514 test "it returns the following in a collection", %{conn: conn} do
1515 user = insert(:user)
1516 user_two = insert(:user)
1517 User.follow(user, user_two)
1521 |> assign(:user, user)
1522 |> get("/users/#{user.nickname}/following")
1523 |> json_response(200)
1525 assert result["first"]["orderedItems"] == [user_two.ap_id]
1528 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1529 user = insert(:user)
1530 user_two = insert(:user, hide_follows: true)
1531 User.follow(user, user_two)
1535 |> assign(:user, user)
1536 |> get("/users/#{user_two.nickname}/following")
1537 |> json_response(200)
1539 assert is_binary(result["first"])
1542 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1544 user = insert(:user)
1545 user_two = insert(:user, hide_follows: true)
1549 |> assign(:user, user)
1550 |> get("/users/#{user_two.nickname}/following?page=1")
1552 assert result.status == 403
1553 assert result.resp_body == ""
1556 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1558 user = insert(:user, hide_follows: true)
1559 other_user = insert(:user)
1560 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1564 |> assign(:user, user)
1565 |> get("/users/#{user.nickname}/following?page=1")
1566 |> json_response(200)
1568 assert result["totalItems"] == 1
1569 assert result["orderedItems"] == [other_user.ap_id]
1572 test "it works for more than 10 users", %{conn: conn} do
1573 user = insert(:user)
1575 Enum.each(1..15, fn _ ->
1576 user = User.get_cached_by_id(user.id)
1577 other_user = insert(:user)
1578 User.follow(user, other_user)
1583 |> assign(:user, user)
1584 |> get("/users/#{user.nickname}/following")
1585 |> json_response(200)
1587 assert length(result["first"]["orderedItems"]) == 10
1588 assert result["first"]["totalItems"] == 15
1589 assert result["totalItems"] == 15
1593 |> assign(:user, user)
1594 |> get("/users/#{user.nickname}/following?page=2")
1595 |> json_response(200)
1597 assert length(result["orderedItems"]) == 5
1598 assert result["totalItems"] == 15
1601 test "does not require authentication", %{conn: conn} do
1602 user = insert(:user)
1605 |> get("/users/#{user.nickname}/following")
1606 |> json_response(200)
1610 describe "delivery tracking" do
1611 test "it tracks a signed object fetch", %{conn: conn} do
1612 user = insert(:user, local: false)
1613 activity = insert(:note_activity)
1614 object = Object.normalize(activity, fetch: false)
1616 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1619 |> put_req_header("accept", "application/activity+json")
1620 |> assign(:user, user)
1622 |> json_response(200)
1624 assert Delivery.get(object.id, user.id)
1627 test "it tracks a signed activity fetch", %{conn: conn} do
1628 user = insert(:user, local: false)
1629 activity = insert(:note_activity)
1630 object = Object.normalize(activity, fetch: false)
1632 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1635 |> put_req_header("accept", "application/activity+json")
1636 |> assign(:user, user)
1637 |> get(activity_path)
1638 |> json_response(200)
1640 assert Delivery.get(object.id, user.id)
1643 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1644 user = insert(:user, local: false)
1645 other_user = insert(:user, local: false)
1646 activity = insert(:note_activity)
1647 object = Object.normalize(activity, fetch: false)
1649 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1652 |> put_req_header("accept", "application/activity+json")
1653 |> assign(:user, user)
1655 |> json_response(200)
1658 |> put_req_header("accept", "application/activity+json")
1659 |> assign(:user, other_user)
1661 |> json_response(200)
1663 assert Delivery.get(object.id, user.id)
1664 assert Delivery.get(object.id, other_user.id)
1667 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1668 user = insert(:user, local: false)
1669 other_user = insert(:user, local: false)
1670 activity = insert(:note_activity)
1671 object = Object.normalize(activity, fetch: false)
1673 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1676 |> put_req_header("accept", "application/activity+json")
1677 |> assign(:user, user)
1678 |> get(activity_path)
1679 |> json_response(200)
1682 |> put_req_header("accept", "application/activity+json")
1683 |> assign(:user, other_user)
1684 |> get(activity_path)
1685 |> json_response(200)
1687 assert Delivery.get(object.id, user.id)
1688 assert Delivery.get(object.id, other_user.id)
1692 describe "Additional ActivityPub C2S endpoints" do
1693 test "GET /api/ap/whoami", %{conn: conn} do
1694 user = insert(:user)
1698 |> assign(:user, user)
1699 |> get("/api/ap/whoami")
1701 user = User.get_cached_by_id(user.id)
1703 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1706 |> get("/api/ap/whoami")
1707 |> json_response(403)
1710 setup do: clear_config([:media_proxy])
1711 setup do: clear_config([Pleroma.Upload])
1713 test "POST /api/ap/upload_media", %{conn: conn} do
1714 user = insert(:user)
1716 desc = "Description of the image"
1718 image = %Plug.Upload{
1719 content_type: "image/jpeg",
1720 path: Path.absname("test/fixtures/image.jpg"),
1721 filename: "an_image.jpg"
1726 |> assign(:user, user)
1727 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1728 |> json_response(:created)
1730 assert object["name"] == desc
1731 assert object["type"] == "Document"
1732 assert object["actor"] == user.ap_id
1733 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1734 assert is_binary(object_href)
1735 assert object_mediatype == "image/jpeg"
1736 assert String.ends_with?(object_href, ".jpg")
1738 activity_request = %{
1739 "@context" => "https://www.w3.org/ns/activitystreams",
1743 "content" => "AP C2S test, attachment",
1744 "attachment" => [object]
1746 "to" => "https://www.w3.org/ns/activitystreams#Public",
1752 |> assign(:user, user)
1753 |> post("/users/#{user.nickname}/outbox", activity_request)
1754 |> json_response(:created)
1756 assert activity_response["id"]
1757 assert activity_response["object"]
1758 assert activity_response["actor"] == user.ap_id
1760 assert %Object{data: %{"attachment" => [attachment]}} =
1761 Object.normalize(activity_response["object"], fetch: false)
1763 assert attachment["type"] == "Document"
1764 assert attachment["name"] == desc
1768 "href" => ^object_href,
1770 "mediaType" => ^object_mediatype
1772 ] = attachment["url"]
1774 # Fails if unauthenticated
1776 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1777 |> json_response(403)