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", "attridbutedTo"], 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 test "it removes all follower collections but actor's", %{conn: conn} do
824 [actor, recipient] = insert_pair(:user)
827 File.read!("test/fixtures/activitypub-client-post-activity.json")
830 object = Map.put(data["object"], "attributedTo", actor.ap_id)
834 |> Map.put("id", Utils.generate_object_id())
835 |> Map.put("actor", actor.ap_id)
836 |> Map.put("object", object)
838 recipient.follower_address,
839 actor.follower_address
843 recipient.follower_address,
844 "https://www.w3.org/ns/activitystreams#Public"
848 |> assign(:valid_signature, true)
849 |> put_req_header("content-type", "application/activity+json")
850 |> post("/users/#{recipient.nickname}/inbox", data)
851 |> json_response(200)
853 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
855 activity = Activity.get_by_ap_id(data["id"])
858 assert actor.follower_address in activity.recipients
859 assert actor.follower_address in activity.data["cc"]
861 refute recipient.follower_address in activity.recipients
862 refute recipient.follower_address in activity.data["cc"]
863 refute recipient.follower_address in activity.data["to"]
866 test "it requires authentication", %{conn: conn} do
868 conn = put_req_header(conn, "accept", "application/activity+json")
870 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
871 assert json_response(ret_conn, 403)
875 |> assign(:user, user)
876 |> get("/users/#{user.nickname}/inbox")
878 assert json_response(ret_conn, 200)
881 @tag capture_log: true
882 test "forwarded report", %{conn: conn} do
883 admin = insert(:user, is_admin: true)
884 actor = insert(:user, local: false)
885 remote_domain = URI.parse(actor.ap_id).host
886 reported_user = insert(:user)
888 note = insert(:note_activity, user: reported_user)
892 "https://www.w3.org/ns/activitystreams",
893 "https://#{remote_domain}/schemas/litepub-0.1.jsonld",
898 "actor" => actor.ap_id,
903 "context" => "context",
904 "id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
905 "nickname" => reported_user.nickname,
910 "actor_type" => "Person",
911 "approval_pending" => false,
913 "confirmation_pending" => false,
914 "deactivated" => false,
915 "display_name" => "test user",
916 "id" => reported_user.id,
918 "nickname" => reported_user.nickname,
919 "registration_reason" => nil,
925 "url" => reported_user.ap_id
928 "id" => note.data["id"],
929 "published" => note.data["published"],
933 "published" => note.data["published"],
940 |> assign(:valid_signature, true)
941 |> put_req_header("content-type", "application/activity+json")
942 |> post("/users/#{reported_user.nickname}/inbox", data)
943 |> json_response(200)
945 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
947 assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
949 ObanHelpers.perform_all()
951 Swoosh.TestAssertions.assert_email_sent(
952 to: {admin.name, admin.email},
953 html_body: ~r/Reported Account:/i
957 @tag capture_log: true
958 test "forwarded report from mastodon", %{conn: conn} do
959 admin = insert(:user, is_admin: true)
960 actor = insert(:user, local: false)
961 remote_domain = URI.parse(actor.ap_id).host
962 remote_actor = "https://#{remote_domain}/actor"
963 [reported_user, another] = insert_list(2, :user)
965 note = insert(:note_activity, user: reported_user)
967 Pleroma.Web.CommonAPI.favorite(another, note.id)
970 "test/fixtures/mastodon/application_actor.json"
972 |> String.replace("{{DOMAIN}}", remote_domain)
974 Tesla.Mock.mock(fn %{url: ^remote_actor} ->
977 body: mock_json_body,
978 headers: [{"content-type", "application/activity+json"}]
983 "@context" => "https://www.w3.org/ns/activitystreams",
984 "actor" => remote_actor,
985 "content" => "test report",
986 "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
987 "nickname" => reported_user.nickname,
996 |> assign(:valid_signature, true)
997 |> put_req_header("content-type", "application/activity+json")
998 |> post("/users/#{reported_user.nickname}/inbox", data)
999 |> json_response(200)
1001 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
1003 flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
1004 reported_user_ap_id = reported_user.ap_id
1006 [^reported_user_ap_id, flag_data] = flag_activity.data["object"]
1008 Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
1009 ObanHelpers.perform_all()
1011 Swoosh.TestAssertions.assert_email_sent(
1012 to: {admin.name, admin.email},
1013 html_body: ~r/#{note.data["object"]}/i
1018 describe "GET /users/:nickname/outbox" do
1019 test "it paginates correctly", %{conn: conn} do
1020 user = insert(:user)
1021 conn = assign(conn, :user, user)
1022 outbox_endpoint = user.ap_id <> "/outbox"
1026 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
1032 |> put_req_header("accept", "application/activity+json")
1033 |> get(outbox_endpoint <> "?page=true")
1034 |> json_response(200)
1036 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
1037 assert length(result["orderedItems"]) == 20
1038 assert length(result_ids) == 20
1039 assert result["next"]
1040 assert String.starts_with?(result["next"], outbox_endpoint)
1044 |> put_req_header("accept", "application/activity+json")
1045 |> get(result["next"])
1046 |> json_response(200)
1048 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
1049 assert length(result_next["orderedItems"]) == 6
1050 assert length(result_next_ids) == 6
1051 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
1052 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
1053 assert String.starts_with?(result["id"], outbox_endpoint)
1057 |> put_req_header("accept", "application/activity+json")
1058 |> get(result_next["id"])
1059 |> json_response(200)
1061 assert result_next == result_next_again
1064 test "it returns 200 even if there're no activities", %{conn: conn} do
1065 user = insert(:user)
1066 outbox_endpoint = user.ap_id <> "/outbox"
1070 |> assign(:user, user)
1071 |> put_req_header("accept", "application/activity+json")
1072 |> get(outbox_endpoint)
1074 result = json_response(conn, 200)
1075 assert outbox_endpoint == result["id"]
1078 test "it returns a note activity in a collection", %{conn: conn} do
1079 note_activity = insert(:note_activity)
1080 note_object = Object.normalize(note_activity, fetch: false)
1081 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1085 |> assign(:user, user)
1086 |> put_req_header("accept", "application/activity+json")
1087 |> get("/users/#{user.nickname}/outbox?page=true")
1089 assert response(conn, 200) =~ note_object.data["content"]
1092 test "it returns an announce activity in a collection", %{conn: conn} do
1093 announce_activity = insert(:announce_activity)
1094 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
1098 |> assign(:user, user)
1099 |> put_req_header("accept", "application/activity+json")
1100 |> get("/users/#{user.nickname}/outbox?page=true")
1102 assert response(conn, 200) =~ announce_activity.data["object"]
1105 test "It returns poll Answers when authenticated", %{conn: conn} do
1106 poller = insert(:user)
1107 voter = insert(:user)
1110 CommonAPI.post(poller, %{
1112 poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
1115 assert question = Object.normalize(activity, fetch: false)
1117 {:ok, [activity], _object} = CommonAPI.vote(voter, question, [1])
1121 |> assign(:user, voter)
1122 |> put_req_header("accept", "application/activity+json")
1123 |> get(voter.ap_id <> "/outbox?page=true")
1124 |> json_response(200)
1126 assert [answer_outbox] = outbox_get["orderedItems"]
1127 assert answer_outbox["id"] == activity.data["id"]
1131 describe "POST /users/:nickname/outbox (C2S)" do
1132 setup do: clear_config([:instance, :limit])
1137 "@context" => "https://www.w3.org/ns/activitystreams",
1139 "object" => %{"type" => "Note", "content" => "AP C2S test"},
1140 "to" => "https://www.w3.org/ns/activitystreams#Public",
1146 test "it rejects posts from other users / unauthenticated users", %{
1150 user = insert(:user)
1151 other_user = insert(:user)
1152 conn = put_req_header(conn, "content-type", "application/activity+json")
1155 |> post("/users/#{user.nickname}/outbox", activity)
1156 |> json_response(403)
1159 |> assign(:user, other_user)
1160 |> post("/users/#{user.nickname}/outbox", activity)
1161 |> json_response(403)
1164 test "it inserts an incoming create activity into the database", %{
1168 user = insert(:user)
1172 |> assign(:user, user)
1173 |> put_req_header("content-type", "application/activity+json")
1174 |> post("/users/#{user.nickname}/outbox", activity)
1175 |> json_response(201)
1177 assert Activity.get_by_ap_id(result["id"])
1178 assert result["object"]
1179 assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
1180 assert object["content"] == activity["object"]["content"]
1183 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1184 user = insert(:user)
1188 |> put_in(["object", "type"], "Benis")
1192 |> assign(:user, user)
1193 |> put_req_header("content-type", "application/activity+json")
1194 |> post("/users/#{user.nickname}/outbox", activity)
1195 |> json_response(400)
1198 test "it inserts an incoming sensitive activity into the database", %{
1202 user = insert(:user)
1203 conn = assign(conn, :user, user)
1204 object = Map.put(activity["object"], "sensitive", true)
1205 activity = Map.put(activity, "object", object)
1209 |> put_req_header("content-type", "application/activity+json")
1210 |> post("/users/#{user.nickname}/outbox", activity)
1211 |> json_response(201)
1213 assert Activity.get_by_ap_id(response["id"])
1214 assert response["object"]
1215 assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false)
1216 assert response_object["sensitive"] == true
1217 assert response_object["content"] == activity["object"]["content"]
1221 |> put_req_header("accept", "application/activity+json")
1222 |> get(response["id"])
1223 |> json_response(200)
1225 assert representation["object"]["sensitive"] == true
1228 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1229 user = insert(:user)
1230 activity = Map.put(activity, "type", "BadType")
1234 |> assign(:user, user)
1235 |> put_req_header("content-type", "application/activity+json")
1236 |> post("/users/#{user.nickname}/outbox", activity)
1238 assert json_response(conn, 400)
1241 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1242 note_activity = insert(:note_activity)
1243 note_object = Object.normalize(note_activity, fetch: false)
1244 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1249 id: note_object.data["id"]
1255 |> assign(:user, user)
1256 |> put_req_header("content-type", "application/activity+json")
1257 |> post("/users/#{user.nickname}/outbox", data)
1259 result = json_response(conn, 201)
1260 assert Activity.get_by_ap_id(result["id"])
1262 assert object = Object.get_by_ap_id(note_object.data["id"])
1263 assert object.data["type"] == "Tombstone"
1266 test "it rejects delete activity of object from other actor", %{conn: conn} do
1267 note_activity = insert(:note_activity)
1268 note_object = Object.normalize(note_activity, fetch: false)
1269 user = insert(:user)
1274 id: note_object.data["id"]
1280 |> assign(:user, user)
1281 |> put_req_header("content-type", "application/activity+json")
1282 |> post("/users/#{user.nickname}/outbox", data)
1284 assert json_response(conn, 400)
1287 test "it increases like count when receiving a like action", %{conn: conn} do
1288 note_activity = insert(:note_activity)
1289 note_object = Object.normalize(note_activity, fetch: false)
1290 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1295 id: note_object.data["id"]
1301 |> assign(:user, user)
1302 |> put_req_header("content-type", "application/activity+json")
1303 |> post("/users/#{user.nickname}/outbox", data)
1305 result = json_response(conn, 201)
1306 assert Activity.get_by_ap_id(result["id"])
1308 assert object = Object.get_by_ap_id(note_object.data["id"])
1309 assert object.data["like_count"] == 1
1312 test "it doesn't spreads faulty attributedTo or actor fields", %{
1316 reimu = insert(:user, nickname: "reimu")
1317 cirno = insert(:user, nickname: "cirno")
1324 |> put_in(["object", "actor"], reimu.ap_id)
1325 |> put_in(["object", "attributedTo"], reimu.ap_id)
1326 |> put_in(["actor"], reimu.ap_id)
1327 |> put_in(["attributedTo"], reimu.ap_id)
1331 |> assign(:user, cirno)
1332 |> put_req_header("content-type", "application/activity+json")
1333 |> post("/users/#{reimu.nickname}/outbox", activity)
1334 |> json_response(403)
1338 |> assign(:user, cirno)
1339 |> put_req_header("content-type", "application/activity+json")
1340 |> post("/users/#{cirno.nickname}/outbox", activity)
1341 |> json_response(201)
1343 assert cirno_outbox["attributedTo"] == nil
1344 assert cirno_outbox["actor"] == cirno.ap_id
1346 assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false)
1347 assert cirno_object.data["actor"] == cirno.ap_id
1348 assert cirno_object.data["attributedTo"] == cirno.ap_id
1351 test "Character limitation", %{conn: conn, activity: activity} do
1352 clear_config([:instance, :limit], 5)
1353 user = insert(:user)
1357 |> assign(:user, user)
1358 |> put_req_header("content-type", "application/activity+json")
1359 |> post("/users/#{user.nickname}/outbox", activity)
1360 |> json_response(400)
1362 assert result == "Note is over the character limit"
1366 describe "/relay/followers" do
1367 test "it returns relay followers", %{conn: conn} do
1368 relay_actor = Relay.get_actor()
1369 user = insert(:user)
1370 User.follow(user, relay_actor)
1374 |> get("/relay/followers")
1375 |> json_response(200)
1377 assert result["first"]["orderedItems"] == [user.ap_id]
1380 test "on non-federating instance, it returns 404", %{conn: conn} do
1381 clear_config([:instance, :federating], false)
1382 user = insert(:user)
1385 |> assign(:user, user)
1386 |> get("/relay/followers")
1387 |> json_response(404)
1391 describe "/relay/following" do
1392 test "it returns relay following", %{conn: conn} do
1395 |> get("/relay/following")
1396 |> json_response(200)
1398 assert result["first"]["orderedItems"] == []
1401 test "on non-federating instance, it returns 404", %{conn: conn} do
1402 clear_config([:instance, :federating], false)
1403 user = insert(:user)
1406 |> assign(:user, user)
1407 |> get("/relay/following")
1408 |> json_response(404)
1412 describe "/users/:nickname/followers" do
1413 test "it returns the followers in a collection", %{conn: conn} do
1414 user = insert(:user)
1415 user_two = insert(:user)
1416 User.follow(user, user_two)
1420 |> assign(:user, user_two)
1421 |> get("/users/#{user_two.nickname}/followers")
1422 |> json_response(200)
1424 assert result["first"]["orderedItems"] == [user.ap_id]
1427 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1428 user = insert(:user)
1429 user_two = insert(:user, hide_followers: true)
1430 User.follow(user, user_two)
1434 |> assign(:user, user)
1435 |> get("/users/#{user_two.nickname}/followers")
1436 |> json_response(200)
1438 assert is_binary(result["first"])
1441 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1443 user = insert(:user)
1444 other_user = insert(:user, hide_followers: true)
1448 |> assign(:user, user)
1449 |> get("/users/#{other_user.nickname}/followers?page=1")
1451 assert result.status == 403
1452 assert result.resp_body == ""
1455 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1457 user = insert(:user, hide_followers: true)
1458 other_user = insert(:user)
1459 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1463 |> assign(:user, user)
1464 |> get("/users/#{user.nickname}/followers?page=1")
1465 |> json_response(200)
1467 assert result["totalItems"] == 1
1468 assert result["orderedItems"] == [other_user.ap_id]
1471 test "it works for more than 10 users", %{conn: conn} do
1472 user = insert(:user)
1474 Enum.each(1..15, fn _ ->
1475 other_user = insert(:user)
1476 User.follow(other_user, user)
1481 |> assign(:user, user)
1482 |> get("/users/#{user.nickname}/followers")
1483 |> json_response(200)
1485 assert length(result["first"]["orderedItems"]) == 10
1486 assert result["first"]["totalItems"] == 15
1487 assert result["totalItems"] == 15
1491 |> assign(:user, user)
1492 |> get("/users/#{user.nickname}/followers?page=2")
1493 |> json_response(200)
1495 assert length(result["orderedItems"]) == 5
1496 assert result["totalItems"] == 15
1499 test "does not require authentication", %{conn: conn} do
1500 user = insert(:user)
1503 |> get("/users/#{user.nickname}/followers")
1504 |> json_response(200)
1508 describe "/users/:nickname/following" do
1509 test "it returns the following in a collection", %{conn: conn} do
1510 user = insert(:user)
1511 user_two = insert(:user)
1512 User.follow(user, user_two)
1516 |> assign(:user, user)
1517 |> get("/users/#{user.nickname}/following")
1518 |> json_response(200)
1520 assert result["first"]["orderedItems"] == [user_two.ap_id]
1523 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1524 user = insert(:user)
1525 user_two = insert(:user, hide_follows: true)
1526 User.follow(user, user_two)
1530 |> assign(:user, user)
1531 |> get("/users/#{user_two.nickname}/following")
1532 |> json_response(200)
1534 assert is_binary(result["first"])
1537 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1539 user = insert(:user)
1540 user_two = insert(:user, hide_follows: true)
1544 |> assign(:user, user)
1545 |> get("/users/#{user_two.nickname}/following?page=1")
1547 assert result.status == 403
1548 assert result.resp_body == ""
1551 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1553 user = insert(:user, hide_follows: true)
1554 other_user = insert(:user)
1555 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1559 |> assign(:user, user)
1560 |> get("/users/#{user.nickname}/following?page=1")
1561 |> json_response(200)
1563 assert result["totalItems"] == 1
1564 assert result["orderedItems"] == [other_user.ap_id]
1567 test "it works for more than 10 users", %{conn: conn} do
1568 user = insert(:user)
1570 Enum.each(1..15, fn _ ->
1571 user = User.get_cached_by_id(user.id)
1572 other_user = insert(:user)
1573 User.follow(user, other_user)
1578 |> assign(:user, user)
1579 |> get("/users/#{user.nickname}/following")
1580 |> json_response(200)
1582 assert length(result["first"]["orderedItems"]) == 10
1583 assert result["first"]["totalItems"] == 15
1584 assert result["totalItems"] == 15
1588 |> assign(:user, user)
1589 |> get("/users/#{user.nickname}/following?page=2")
1590 |> json_response(200)
1592 assert length(result["orderedItems"]) == 5
1593 assert result["totalItems"] == 15
1596 test "does not require authentication", %{conn: conn} do
1597 user = insert(:user)
1600 |> get("/users/#{user.nickname}/following")
1601 |> json_response(200)
1605 describe "delivery tracking" do
1606 test "it tracks a signed object fetch", %{conn: conn} do
1607 user = insert(:user, local: false)
1608 activity = insert(:note_activity)
1609 object = Object.normalize(activity, fetch: false)
1611 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1614 |> put_req_header("accept", "application/activity+json")
1615 |> assign(:user, user)
1617 |> json_response(200)
1619 assert Delivery.get(object.id, user.id)
1622 test "it tracks a signed activity fetch", %{conn: conn} do
1623 user = insert(:user, local: false)
1624 activity = insert(:note_activity)
1625 object = Object.normalize(activity, fetch: false)
1627 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1630 |> put_req_header("accept", "application/activity+json")
1631 |> assign(:user, user)
1632 |> get(activity_path)
1633 |> json_response(200)
1635 assert Delivery.get(object.id, user.id)
1638 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1639 user = insert(:user, local: false)
1640 other_user = insert(:user, local: false)
1641 activity = insert(:note_activity)
1642 object = Object.normalize(activity, fetch: false)
1644 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1647 |> put_req_header("accept", "application/activity+json")
1648 |> assign(:user, user)
1650 |> json_response(200)
1653 |> put_req_header("accept", "application/activity+json")
1654 |> assign(:user, other_user)
1656 |> json_response(200)
1658 assert Delivery.get(object.id, user.id)
1659 assert Delivery.get(object.id, other_user.id)
1662 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1663 user = insert(:user, local: false)
1664 other_user = insert(:user, local: false)
1665 activity = insert(:note_activity)
1666 object = Object.normalize(activity, fetch: false)
1668 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1671 |> put_req_header("accept", "application/activity+json")
1672 |> assign(:user, user)
1673 |> get(activity_path)
1674 |> json_response(200)
1677 |> put_req_header("accept", "application/activity+json")
1678 |> assign(:user, other_user)
1679 |> get(activity_path)
1680 |> json_response(200)
1682 assert Delivery.get(object.id, user.id)
1683 assert Delivery.get(object.id, other_user.id)
1687 describe "Additional ActivityPub C2S endpoints" do
1688 test "GET /api/ap/whoami", %{conn: conn} do
1689 user = insert(:user)
1693 |> assign(:user, user)
1694 |> get("/api/ap/whoami")
1696 user = User.get_cached_by_id(user.id)
1698 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1701 |> get("/api/ap/whoami")
1702 |> json_response(403)
1705 setup do: clear_config([:media_proxy])
1706 setup do: clear_config([Pleroma.Upload])
1708 test "POST /api/ap/upload_media", %{conn: conn} do
1709 user = insert(:user)
1711 desc = "Description of the image"
1713 image = %Plug.Upload{
1714 content_type: "image/jpeg",
1715 path: Path.absname("test/fixtures/image.jpg"),
1716 filename: "an_image.jpg"
1721 |> assign(:user, user)
1722 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1723 |> json_response(:created)
1725 assert object["name"] == desc
1726 assert object["type"] == "Document"
1727 assert object["actor"] == user.ap_id
1728 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1729 assert is_binary(object_href)
1730 assert object_mediatype == "image/jpeg"
1731 assert String.ends_with?(object_href, ".jpg")
1733 activity_request = %{
1734 "@context" => "https://www.w3.org/ns/activitystreams",
1738 "content" => "AP C2S test, attachment",
1739 "attachment" => [object]
1741 "to" => "https://www.w3.org/ns/activitystreams#Public",
1747 |> assign(:user, user)
1748 |> post("/users/#{user.nickname}/outbox", activity_request)
1749 |> json_response(:created)
1751 assert activity_response["id"]
1752 assert activity_response["object"]
1753 assert activity_response["actor"] == user.ap_id
1755 assert %Object{data: %{"attachment" => [attachment]}} =
1756 Object.normalize(activity_response["object"], fetch: false)
1758 assert attachment["type"] == "Document"
1759 assert attachment["name"] == desc
1763 "href" => ^object_href,
1765 "mediaType" => ^object_mediatype
1767 ] = attachment["url"]
1769 # Fails if unauthenticated
1771 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1772 |> json_response(403)