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 returns a json representation of the object with accept application/json", %{
220 uuid = String.split(note.data["id"], "/") |> List.last()
224 |> put_req_header("accept", "application/json")
225 |> get("/objects/#{uuid}")
227 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
230 test "it returns a json representation of the object with accept application/activity+json",
233 uuid = String.split(note.data["id"], "/") |> List.last()
237 |> put_req_header("accept", "application/activity+json")
238 |> get("/objects/#{uuid}")
240 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
243 test "it returns a json representation of the object with accept application/ld+json", %{
247 uuid = String.split(note.data["id"], "/") |> List.last()
253 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
255 |> get("/objects/#{uuid}")
257 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
260 test "it returns 404 for non-public messages", %{conn: conn} do
261 note = insert(:direct_note)
262 uuid = String.split(note.data["id"], "/") |> List.last()
266 |> put_req_header("accept", "application/activity+json")
267 |> get("/objects/#{uuid}")
269 assert json_response(conn, 404)
272 test "it returns 404 for tombstone objects", %{conn: conn} do
273 tombstone = insert(:tombstone)
274 uuid = String.split(tombstone.data["id"], "/") |> List.last()
278 |> put_req_header("accept", "application/activity+json")
279 |> get("/objects/#{uuid}")
281 assert json_response(conn, 404)
284 test "it caches a response", %{conn: conn} do
286 uuid = String.split(note.data["id"], "/") |> List.last()
290 |> put_req_header("accept", "application/activity+json")
291 |> get("/objects/#{uuid}")
293 assert json_response(conn1, :ok)
294 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
298 |> put_req_header("accept", "application/activity+json")
299 |> get("/objects/#{uuid}")
301 assert json_response(conn1, :ok) == json_response(conn2, :ok)
302 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
305 test "cached purged after object deletion", %{conn: conn} do
307 uuid = String.split(note.data["id"], "/") |> List.last()
311 |> put_req_header("accept", "application/activity+json")
312 |> get("/objects/#{uuid}")
314 assert json_response(conn1, :ok)
315 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
321 |> put_req_header("accept", "application/activity+json")
322 |> get("/objects/#{uuid}")
324 assert "Not found" == json_response(conn2, :not_found)
328 describe "/activities/:uuid" do
329 test "it returns a json representation of the activity", %{conn: conn} do
330 activity = insert(:note_activity)
331 uuid = String.split(activity.data["id"], "/") |> List.last()
335 |> put_req_header("accept", "application/activity+json")
336 |> get("/activities/#{uuid}")
338 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
341 test "it returns 404 for non-public activities", %{conn: conn} do
342 activity = insert(:direct_note_activity)
343 uuid = String.split(activity.data["id"], "/") |> List.last()
347 |> put_req_header("accept", "application/activity+json")
348 |> get("/activities/#{uuid}")
350 assert json_response(conn, 404)
353 test "it caches a response", %{conn: conn} do
354 activity = insert(:note_activity)
355 uuid = String.split(activity.data["id"], "/") |> List.last()
359 |> put_req_header("accept", "application/activity+json")
360 |> get("/activities/#{uuid}")
362 assert json_response(conn1, :ok)
363 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
367 |> put_req_header("accept", "application/activity+json")
368 |> get("/activities/#{uuid}")
370 assert json_response(conn1, :ok) == json_response(conn2, :ok)
371 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
374 test "cached purged after activity deletion", %{conn: conn} do
376 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
378 uuid = String.split(activity.data["id"], "/") |> List.last()
382 |> put_req_header("accept", "application/activity+json")
383 |> get("/activities/#{uuid}")
385 assert json_response(conn1, :ok)
386 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
388 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
392 |> put_req_header("accept", "application/activity+json")
393 |> get("/activities/#{uuid}")
395 assert "Not found" == json_response(conn2, :not_found)
400 test "it inserts an incoming activity into the database", %{conn: conn} do
401 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
405 |> assign(:valid_signature, true)
406 |> put_req_header("content-type", "application/activity+json")
407 |> post("/inbox", data)
409 assert "ok" == json_response(conn, 200)
411 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
412 assert Activity.get_by_ap_id(data["id"])
415 @tag capture_log: true
416 test "it inserts an incoming activity into the database" <>
417 "even if we can't fetch the user but have it in our db",
421 ap_id: "https://mastodon.example.org/users/raymoo",
424 last_refreshed_at: nil
428 File.read!("test/fixtures/mastodon-post-activity.json")
430 |> Map.put("actor", user.ap_id)
431 |> put_in(["object", "attridbutedTo"], user.ap_id)
435 |> assign(:valid_signature, true)
436 |> put_req_header("content-type", "application/activity+json")
437 |> post("/inbox", data)
439 assert "ok" == json_response(conn, 200)
441 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
442 assert Activity.get_by_ap_id(data["id"])
445 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
446 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
448 sender_url = data["actor"]
449 Instances.set_consistently_unreachable(sender_url)
450 refute Instances.reachable?(sender_url)
454 |> assign(:valid_signature, true)
455 |> put_req_header("content-type", "application/activity+json")
456 |> post("/inbox", data)
458 assert "ok" == json_response(conn, 200)
459 assert Instances.reachable?(sender_url)
462 test "accept follow activity", %{conn: conn} do
463 Pleroma.Config.put([:instance, :federating], true)
464 relay = Relay.get_actor()
466 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
468 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
469 relay = refresh_record(relay)
472 File.read!("test/fixtures/relay/accept-follow.json")
473 |> String.replace("{{ap_id}}", relay.ap_id)
474 |> String.replace("{{activity_id}}", activity.data["id"])
478 |> assign(:valid_signature, true)
479 |> put_req_header("content-type", "application/activity+json")
480 |> post("/inbox", accept)
481 |> json_response(200)
483 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
485 assert Pleroma.FollowingRelationship.following?(
490 Mix.shell(Mix.Shell.Process)
493 Mix.shell(Mix.Shell.IO)
496 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
497 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
500 @tag capture_log: true
501 test "without valid signature, " <>
502 "it only accepts Create activities and requires enabled federation",
504 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
505 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
507 conn = put_req_header(conn, "content-type", "application/activity+json")
509 Config.put([:instance, :federating], false)
512 |> post("/inbox", data)
513 |> json_response(403)
516 |> post("/inbox", non_create_data)
517 |> json_response(403)
519 Config.put([:instance, :federating], true)
521 ret_conn = post(conn, "/inbox", data)
522 assert "ok" == json_response(ret_conn, 200)
525 |> post("/inbox", non_create_data)
526 |> json_response(400)
530 describe "/users/:nickname/inbox" do
533 File.read!("test/fixtures/mastodon-post-activity.json")
539 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
541 data = Map.put(data, "bcc", [user.ap_id])
545 |> assign(:valid_signature, true)
546 |> put_req_header("content-type", "application/activity+json")
547 |> post("/users/#{user.nickname}/inbox", data)
549 assert "ok" == json_response(conn, 200)
550 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
551 assert Activity.get_by_ap_id(data["id"])
554 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
558 Map.put(data, "to", user.ap_id)
563 |> assign(:valid_signature, true)
564 |> put_req_header("content-type", "application/activity+json")
565 |> post("/users/#{user.nickname}/inbox", data)
567 assert "ok" == json_response(conn, 200)
568 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
569 assert Activity.get_by_ap_id(data["id"])
572 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
576 Map.put(data, "cc", user.ap_id)
581 |> assign(:valid_signature, true)
582 |> put_req_header("content-type", "application/activity+json")
583 |> post("/users/#{user.nickname}/inbox", data)
585 assert "ok" == json_response(conn, 200)
586 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
587 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
588 assert user.ap_id in activity.recipients
591 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
595 Map.put(data, "bcc", user.ap_id)
601 |> assign(:valid_signature, true)
602 |> put_req_header("content-type", "application/activity+json")
603 |> post("/users/#{user.nickname}/inbox", data)
605 assert "ok" == json_response(conn, 200)
606 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
607 assert Activity.get_by_ap_id(data["id"])
610 test "it accepts announces with to as string instead of array", %{conn: conn} do
613 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
614 announcer = insert(:user, local: false)
617 "@context" => "https://www.w3.org/ns/activitystreams",
618 "actor" => announcer.ap_id,
619 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
620 "object" => post.data["object"],
621 "to" => "https://www.w3.org/ns/activitystreams#Public",
622 "cc" => [user.ap_id],
628 |> assign(:valid_signature, true)
629 |> put_req_header("content-type", "application/activity+json")
630 |> post("/users/#{user.nickname}/inbox", data)
632 assert "ok" == json_response(conn, 200)
633 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
634 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
635 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
638 test "it accepts messages from actors that are followed by the user", %{
642 recipient = insert(:user)
643 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
645 {:ok, recipient} = User.follow(recipient, actor)
649 |> Map.put("attributedTo", actor.ap_id)
653 |> Map.put("actor", actor.ap_id)
654 |> Map.put("object", object)
658 |> assign(:valid_signature, true)
659 |> put_req_header("content-type", "application/activity+json")
660 |> post("/users/#{recipient.nickname}/inbox", data)
662 assert "ok" == json_response(conn, 200)
663 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
664 assert Activity.get_by_ap_id(data["id"])
667 test "it rejects reads from other users", %{conn: conn} do
669 other_user = insert(:user)
673 |> assign(:user, other_user)
674 |> put_req_header("accept", "application/activity+json")
675 |> get("/users/#{user.nickname}/inbox")
677 assert json_response(conn, 403)
680 test "it returns a note activity in a collection", %{conn: conn} do
681 note_activity = insert(:direct_note_activity)
682 note_object = Object.normalize(note_activity)
683 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
687 |> assign(:user, user)
688 |> put_req_header("accept", "application/activity+json")
689 |> get("/users/#{user.nickname}/inbox?page=true")
691 assert response(conn, 200) =~ note_object.data["content"]
694 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
696 data = Map.put(data, "bcc", [user.ap_id])
698 sender_host = URI.parse(data["actor"]).host
699 Instances.set_consistently_unreachable(sender_host)
700 refute Instances.reachable?(sender_host)
704 |> assign(:valid_signature, true)
705 |> put_req_header("content-type", "application/activity+json")
706 |> post("/users/#{user.nickname}/inbox", data)
708 assert "ok" == json_response(conn, 200)
709 assert Instances.reachable?(sender_host)
712 test "it removes all follower collections but actor's", %{conn: conn} do
713 [actor, recipient] = insert_pair(:user)
716 File.read!("test/fixtures/activitypub-client-post-activity.json")
719 object = Map.put(data["object"], "attributedTo", actor.ap_id)
723 |> Map.put("id", Utils.generate_object_id())
724 |> Map.put("actor", actor.ap_id)
725 |> Map.put("object", object)
727 recipient.follower_address,
728 actor.follower_address
732 recipient.follower_address,
733 "https://www.w3.org/ns/activitystreams#Public"
737 |> assign(:valid_signature, true)
738 |> put_req_header("content-type", "application/activity+json")
739 |> post("/users/#{recipient.nickname}/inbox", data)
740 |> json_response(200)
742 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
744 activity = Activity.get_by_ap_id(data["id"])
747 assert actor.follower_address in activity.recipients
748 assert actor.follower_address in activity.data["cc"]
750 refute recipient.follower_address in activity.recipients
751 refute recipient.follower_address in activity.data["cc"]
752 refute recipient.follower_address in activity.data["to"]
755 test "it requires authentication", %{conn: conn} do
757 conn = put_req_header(conn, "accept", "application/activity+json")
759 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
760 assert json_response(ret_conn, 403)
764 |> assign(:user, user)
765 |> get("/users/#{user.nickname}/inbox")
767 assert json_response(ret_conn, 200)
770 @tag capture_log: true
771 test "forwarded report", %{conn: conn} do
772 admin = insert(:user, is_admin: true)
773 actor = insert(:user, local: false)
774 remote_domain = URI.parse(actor.ap_id).host
775 reported_user = insert(:user)
777 note = insert(:note_activity, user: reported_user)
781 "https://www.w3.org/ns/activitystreams",
782 "https://#{remote_domain}/schemas/litepub-0.1.jsonld",
787 "actor" => actor.ap_id,
792 "context" => "context",
793 "id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
794 "nickname" => reported_user.nickname,
799 "actor_type" => "Person",
800 "approval_pending" => false,
802 "confirmation_pending" => false,
803 "deactivated" => false,
804 "display_name" => "test user",
805 "id" => reported_user.id,
807 "nickname" => reported_user.nickname,
808 "registration_reason" => nil,
814 "url" => reported_user.ap_id
817 "id" => note.data["id"],
818 "published" => note.data["published"],
822 "published" => note.data["published"],
829 |> assign(:valid_signature, true)
830 |> put_req_header("content-type", "application/activity+json")
831 |> post("/users/#{reported_user.nickname}/inbox", data)
832 |> json_response(200)
834 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
836 assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
838 ObanHelpers.perform_all()
840 Swoosh.TestAssertions.assert_email_sent(
841 to: {admin.name, admin.email},
842 html_body: ~r/Reported Account:/i
846 @tag capture_log: true
847 test "forwarded report from mastodon", %{conn: conn} do
848 admin = insert(:user, is_admin: true)
849 actor = insert(:user, local: false)
850 remote_domain = URI.parse(actor.ap_id).host
851 remote_actor = "https://#{remote_domain}/actor"
852 [reported_user, another] = insert_list(2, :user)
854 note = insert(:note_activity, user: reported_user)
856 Pleroma.Web.CommonAPI.favorite(another, note.id)
859 "test/fixtures/mastodon/application_actor.json"
861 |> String.replace("{{DOMAIN}}", remote_domain)
863 Tesla.Mock.mock(fn %{url: ^remote_actor} ->
866 body: mock_json_body,
867 headers: [{"content-type", "application/activity+json"}]
872 "@context" => "https://www.w3.org/ns/activitystreams",
873 "actor" => remote_actor,
874 "content" => "test report",
875 "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
876 "nickname" => reported_user.nickname,
885 |> assign(:valid_signature, true)
886 |> put_req_header("content-type", "application/activity+json")
887 |> post("/users/#{reported_user.nickname}/inbox", data)
888 |> json_response(200)
890 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
892 flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
893 reported_user_ap_id = reported_user.ap_id
895 [^reported_user_ap_id, flag_data] = flag_activity.data["object"]
897 Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
898 ObanHelpers.perform_all()
900 Swoosh.TestAssertions.assert_email_sent(
901 to: {admin.name, admin.email},
902 html_body: ~r/#{note.data["object"]}/i
907 describe "GET /users/:nickname/outbox" do
908 test "it paginates correctly", %{conn: conn} do
910 conn = assign(conn, :user, user)
911 outbox_endpoint = user.ap_id <> "/outbox"
915 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
921 |> put_req_header("accept", "application/activity+json")
922 |> get(outbox_endpoint <> "?page=true")
923 |> json_response(200)
925 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
926 assert length(result["orderedItems"]) == 20
927 assert length(result_ids) == 20
928 assert result["next"]
929 assert String.starts_with?(result["next"], outbox_endpoint)
933 |> put_req_header("accept", "application/activity+json")
934 |> get(result["next"])
935 |> json_response(200)
937 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
938 assert length(result_next["orderedItems"]) == 6
939 assert length(result_next_ids) == 6
940 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
941 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
942 assert String.starts_with?(result["id"], outbox_endpoint)
946 |> put_req_header("accept", "application/activity+json")
947 |> get(result_next["id"])
948 |> json_response(200)
950 assert result_next == result_next_again
953 test "it returns 200 even if there're no activities", %{conn: conn} do
955 outbox_endpoint = user.ap_id <> "/outbox"
959 |> assign(:user, user)
960 |> put_req_header("accept", "application/activity+json")
961 |> get(outbox_endpoint)
963 result = json_response(conn, 200)
964 assert outbox_endpoint == result["id"]
967 test "it returns a note activity in a collection", %{conn: conn} do
968 note_activity = insert(:note_activity)
969 note_object = Object.normalize(note_activity)
970 user = User.get_cached_by_ap_id(note_activity.data["actor"])
974 |> assign(:user, user)
975 |> put_req_header("accept", "application/activity+json")
976 |> get("/users/#{user.nickname}/outbox?page=true")
978 assert response(conn, 200) =~ note_object.data["content"]
981 test "it returns an announce activity in a collection", %{conn: conn} do
982 announce_activity = insert(:announce_activity)
983 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
987 |> assign(:user, user)
988 |> put_req_header("accept", "application/activity+json")
989 |> get("/users/#{user.nickname}/outbox?page=true")
991 assert response(conn, 200) =~ announce_activity.data["object"]
995 describe "POST /users/:nickname/outbox (C2S)" do
996 setup do: clear_config([:instance, :limit])
1001 "@context" => "https://www.w3.org/ns/activitystreams",
1003 "object" => %{"type" => "Note", "content" => "AP C2S test"},
1004 "to" => "https://www.w3.org/ns/activitystreams#Public",
1010 test "it rejects posts from other users / unauthenticated users", %{
1014 user = insert(:user)
1015 other_user = insert(:user)
1016 conn = put_req_header(conn, "content-type", "application/activity+json")
1019 |> post("/users/#{user.nickname}/outbox", activity)
1020 |> json_response(403)
1023 |> assign(:user, other_user)
1024 |> post("/users/#{user.nickname}/outbox", activity)
1025 |> json_response(403)
1028 test "it inserts an incoming create activity into the database", %{
1032 user = insert(:user)
1036 |> assign(:user, user)
1037 |> put_req_header("content-type", "application/activity+json")
1038 |> post("/users/#{user.nickname}/outbox", activity)
1039 |> json_response(201)
1041 assert Activity.get_by_ap_id(result["id"])
1042 assert result["object"]
1043 assert %Object{data: object} = Object.normalize(result["object"])
1044 assert object["content"] == activity["object"]["content"]
1047 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1048 user = insert(:user)
1052 |> put_in(["object", "type"], "Benis")
1056 |> assign(:user, user)
1057 |> put_req_header("content-type", "application/activity+json")
1058 |> post("/users/#{user.nickname}/outbox", activity)
1059 |> json_response(400)
1062 test "it inserts an incoming sensitive activity into the database", %{
1066 user = insert(:user)
1067 conn = assign(conn, :user, user)
1068 object = Map.put(activity["object"], "sensitive", true)
1069 activity = Map.put(activity, "object", object)
1073 |> put_req_header("content-type", "application/activity+json")
1074 |> post("/users/#{user.nickname}/outbox", activity)
1075 |> json_response(201)
1077 assert Activity.get_by_ap_id(response["id"])
1078 assert response["object"]
1079 assert %Object{data: response_object} = Object.normalize(response["object"])
1080 assert response_object["sensitive"] == true
1081 assert response_object["content"] == activity["object"]["content"]
1085 |> put_req_header("accept", "application/activity+json")
1086 |> get(response["id"])
1087 |> json_response(200)
1089 assert representation["object"]["sensitive"] == true
1092 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1093 user = insert(:user)
1094 activity = Map.put(activity, "type", "BadType")
1098 |> assign(:user, user)
1099 |> put_req_header("content-type", "application/activity+json")
1100 |> post("/users/#{user.nickname}/outbox", activity)
1102 assert json_response(conn, 400)
1105 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1106 note_activity = insert(:note_activity)
1107 note_object = Object.normalize(note_activity)
1108 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1113 id: note_object.data["id"]
1119 |> assign(:user, user)
1120 |> put_req_header("content-type", "application/activity+json")
1121 |> post("/users/#{user.nickname}/outbox", data)
1123 result = json_response(conn, 201)
1124 assert Activity.get_by_ap_id(result["id"])
1126 assert object = Object.get_by_ap_id(note_object.data["id"])
1127 assert object.data["type"] == "Tombstone"
1130 test "it rejects delete activity of object from other actor", %{conn: conn} do
1131 note_activity = insert(:note_activity)
1132 note_object = Object.normalize(note_activity)
1133 user = insert(:user)
1138 id: note_object.data["id"]
1144 |> assign(:user, user)
1145 |> put_req_header("content-type", "application/activity+json")
1146 |> post("/users/#{user.nickname}/outbox", data)
1148 assert json_response(conn, 400)
1151 test "it increases like count when receiving a like action", %{conn: conn} do
1152 note_activity = insert(:note_activity)
1153 note_object = Object.normalize(note_activity)
1154 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1159 id: note_object.data["id"]
1165 |> assign(:user, user)
1166 |> put_req_header("content-type", "application/activity+json")
1167 |> post("/users/#{user.nickname}/outbox", data)
1169 result = json_response(conn, 201)
1170 assert Activity.get_by_ap_id(result["id"])
1172 assert object = Object.get_by_ap_id(note_object.data["id"])
1173 assert object.data["like_count"] == 1
1176 test "it doesn't spreads faulty attributedTo or actor fields", %{
1180 reimu = insert(:user, nickname: "reimu")
1181 cirno = insert(:user, nickname: "cirno")
1188 |> put_in(["object", "actor"], reimu.ap_id)
1189 |> put_in(["object", "attributedTo"], reimu.ap_id)
1190 |> put_in(["actor"], reimu.ap_id)
1191 |> put_in(["attributedTo"], reimu.ap_id)
1195 |> assign(:user, cirno)
1196 |> put_req_header("content-type", "application/activity+json")
1197 |> post("/users/#{reimu.nickname}/outbox", activity)
1198 |> json_response(403)
1202 |> assign(:user, cirno)
1203 |> put_req_header("content-type", "application/activity+json")
1204 |> post("/users/#{cirno.nickname}/outbox", activity)
1205 |> json_response(201)
1207 assert cirno_outbox["attributedTo"] == nil
1208 assert cirno_outbox["actor"] == cirno.ap_id
1210 assert cirno_object = Object.normalize(cirno_outbox["object"])
1211 assert cirno_object.data["actor"] == cirno.ap_id
1212 assert cirno_object.data["attributedTo"] == cirno.ap_id
1215 test "Character limitation", %{conn: conn, activity: activity} do
1216 Pleroma.Config.put([:instance, :limit], 5)
1217 user = insert(:user)
1221 |> assign(:user, user)
1222 |> put_req_header("content-type", "application/activity+json")
1223 |> post("/users/#{user.nickname}/outbox", activity)
1224 |> json_response(400)
1226 assert result == "Note is over the character limit"
1230 describe "/relay/followers" do
1231 test "it returns relay followers", %{conn: conn} do
1232 relay_actor = Relay.get_actor()
1233 user = insert(:user)
1234 User.follow(user, relay_actor)
1238 |> get("/relay/followers")
1239 |> json_response(200)
1241 assert result["first"]["orderedItems"] == [user.ap_id]
1244 test "on non-federating instance, it returns 404", %{conn: conn} do
1245 Config.put([:instance, :federating], false)
1246 user = insert(:user)
1249 |> assign(:user, user)
1250 |> get("/relay/followers")
1251 |> json_response(404)
1255 describe "/relay/following" do
1256 test "it returns relay following", %{conn: conn} do
1259 |> get("/relay/following")
1260 |> json_response(200)
1262 assert result["first"]["orderedItems"] == []
1265 test "on non-federating instance, it returns 404", %{conn: conn} do
1266 Config.put([:instance, :federating], false)
1267 user = insert(:user)
1270 |> assign(:user, user)
1271 |> get("/relay/following")
1272 |> json_response(404)
1276 describe "/users/:nickname/followers" do
1277 test "it returns the followers in a collection", %{conn: conn} do
1278 user = insert(:user)
1279 user_two = insert(:user)
1280 User.follow(user, user_two)
1284 |> assign(:user, user_two)
1285 |> get("/users/#{user_two.nickname}/followers")
1286 |> json_response(200)
1288 assert result["first"]["orderedItems"] == [user.ap_id]
1291 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1292 user = insert(:user)
1293 user_two = insert(:user, hide_followers: true)
1294 User.follow(user, user_two)
1298 |> assign(:user, user)
1299 |> get("/users/#{user_two.nickname}/followers")
1300 |> json_response(200)
1302 assert is_binary(result["first"])
1305 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1307 user = insert(:user)
1308 other_user = insert(:user, hide_followers: true)
1312 |> assign(:user, user)
1313 |> get("/users/#{other_user.nickname}/followers?page=1")
1315 assert result.status == 403
1316 assert result.resp_body == ""
1319 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1321 user = insert(:user, hide_followers: true)
1322 other_user = insert(:user)
1323 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1327 |> assign(:user, user)
1328 |> get("/users/#{user.nickname}/followers?page=1")
1329 |> json_response(200)
1331 assert result["totalItems"] == 1
1332 assert result["orderedItems"] == [other_user.ap_id]
1335 test "it works for more than 10 users", %{conn: conn} do
1336 user = insert(:user)
1338 Enum.each(1..15, fn _ ->
1339 other_user = insert(:user)
1340 User.follow(other_user, user)
1345 |> assign(:user, user)
1346 |> get("/users/#{user.nickname}/followers")
1347 |> json_response(200)
1349 assert length(result["first"]["orderedItems"]) == 10
1350 assert result["first"]["totalItems"] == 15
1351 assert result["totalItems"] == 15
1355 |> assign(:user, user)
1356 |> get("/users/#{user.nickname}/followers?page=2")
1357 |> json_response(200)
1359 assert length(result["orderedItems"]) == 5
1360 assert result["totalItems"] == 15
1363 test "does not require authentication", %{conn: conn} do
1364 user = insert(:user)
1367 |> get("/users/#{user.nickname}/followers")
1368 |> json_response(200)
1372 describe "/users/:nickname/following" do
1373 test "it returns the following in a collection", %{conn: conn} do
1374 user = insert(:user)
1375 user_two = insert(:user)
1376 User.follow(user, user_two)
1380 |> assign(:user, user)
1381 |> get("/users/#{user.nickname}/following")
1382 |> json_response(200)
1384 assert result["first"]["orderedItems"] == [user_two.ap_id]
1387 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1388 user = insert(:user)
1389 user_two = insert(:user, hide_follows: true)
1390 User.follow(user, user_two)
1394 |> assign(:user, user)
1395 |> get("/users/#{user_two.nickname}/following")
1396 |> json_response(200)
1398 assert is_binary(result["first"])
1401 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1403 user = insert(:user)
1404 user_two = insert(:user, hide_follows: true)
1408 |> assign(:user, user)
1409 |> get("/users/#{user_two.nickname}/following?page=1")
1411 assert result.status == 403
1412 assert result.resp_body == ""
1415 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1417 user = insert(:user, hide_follows: true)
1418 other_user = insert(:user)
1419 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1423 |> assign(:user, user)
1424 |> get("/users/#{user.nickname}/following?page=1")
1425 |> json_response(200)
1427 assert result["totalItems"] == 1
1428 assert result["orderedItems"] == [other_user.ap_id]
1431 test "it works for more than 10 users", %{conn: conn} do
1432 user = insert(:user)
1434 Enum.each(1..15, fn _ ->
1435 user = User.get_cached_by_id(user.id)
1436 other_user = insert(:user)
1437 User.follow(user, other_user)
1442 |> assign(:user, user)
1443 |> get("/users/#{user.nickname}/following")
1444 |> json_response(200)
1446 assert length(result["first"]["orderedItems"]) == 10
1447 assert result["first"]["totalItems"] == 15
1448 assert result["totalItems"] == 15
1452 |> assign(:user, user)
1453 |> get("/users/#{user.nickname}/following?page=2")
1454 |> json_response(200)
1456 assert length(result["orderedItems"]) == 5
1457 assert result["totalItems"] == 15
1460 test "does not require authentication", %{conn: conn} do
1461 user = insert(:user)
1464 |> get("/users/#{user.nickname}/following")
1465 |> json_response(200)
1469 describe "delivery tracking" do
1470 test "it tracks a signed object fetch", %{conn: conn} do
1471 user = insert(:user, local: false)
1472 activity = insert(:note_activity)
1473 object = Object.normalize(activity)
1475 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1478 |> put_req_header("accept", "application/activity+json")
1479 |> assign(:user, user)
1481 |> json_response(200)
1483 assert Delivery.get(object.id, user.id)
1486 test "it tracks a signed activity fetch", %{conn: conn} do
1487 user = insert(:user, local: false)
1488 activity = insert(:note_activity)
1489 object = Object.normalize(activity)
1491 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1494 |> put_req_header("accept", "application/activity+json")
1495 |> assign(:user, user)
1496 |> get(activity_path)
1497 |> json_response(200)
1499 assert Delivery.get(object.id, user.id)
1502 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1503 user = insert(:user, local: false)
1504 other_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)
1517 |> put_req_header("accept", "application/activity+json")
1518 |> assign(:user, other_user)
1520 |> json_response(200)
1522 assert Delivery.get(object.id, user.id)
1523 assert Delivery.get(object.id, other_user.id)
1526 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1527 user = insert(:user, local: false)
1528 other_user = insert(:user, local: false)
1529 activity = insert(:note_activity)
1530 object = Object.normalize(activity)
1532 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1535 |> put_req_header("accept", "application/activity+json")
1536 |> assign(:user, user)
1537 |> get(activity_path)
1538 |> json_response(200)
1541 |> put_req_header("accept", "application/activity+json")
1542 |> assign(:user, other_user)
1543 |> get(activity_path)
1544 |> json_response(200)
1546 assert Delivery.get(object.id, user.id)
1547 assert Delivery.get(object.id, other_user.id)
1551 describe "Additional ActivityPub C2S endpoints" do
1552 test "GET /api/ap/whoami", %{conn: conn} do
1553 user = insert(:user)
1557 |> assign(:user, user)
1558 |> get("/api/ap/whoami")
1560 user = User.get_cached_by_id(user.id)
1562 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1565 |> get("/api/ap/whoami")
1566 |> json_response(403)
1569 setup do: clear_config([:media_proxy])
1570 setup do: clear_config([Pleroma.Upload])
1572 test "POST /api/ap/upload_media", %{conn: conn} do
1573 user = insert(:user)
1575 desc = "Description of the image"
1577 image = %Plug.Upload{
1578 content_type: "bad/content-type",
1579 path: Path.absname("test/fixtures/image.jpg"),
1580 filename: "an_image.png"
1585 |> assign(:user, user)
1586 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1587 |> json_response(:created)
1589 assert object["name"] == desc
1590 assert object["type"] == "Document"
1591 assert object["actor"] == user.ap_id
1592 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1593 assert is_binary(object_href)
1594 assert object_mediatype == "image/jpeg"
1595 assert String.ends_with?(object_href, ".jpg")
1597 activity_request = %{
1598 "@context" => "https://www.w3.org/ns/activitystreams",
1602 "content" => "AP C2S test, attachment",
1603 "attachment" => [object]
1605 "to" => "https://www.w3.org/ns/activitystreams#Public",
1611 |> assign(:user, user)
1612 |> post("/users/#{user.nickname}/outbox", activity_request)
1613 |> json_response(:created)
1615 assert activity_response["id"]
1616 assert activity_response["object"]
1617 assert activity_response["actor"] == user.ap_id
1619 assert %Object{data: %{"attachment" => [attachment]}} =
1620 Object.normalize(activity_response["object"])
1622 assert attachment["type"] == "Document"
1623 assert attachment["name"] == desc
1627 "href" => ^object_href,
1629 "mediaType" => ^object_mediatype
1631 ] = attachment["url"]
1633 # Fails if unauthenticated
1635 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1636 |> json_response(403)