1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
6 use Pleroma.Web.ConnCase
7 use Oban.Testing, repo: Pleroma.Repo
11 alias Pleroma.Delivery
12 alias Pleroma.Instances
14 alias Pleroma.Tests.ObanHelpers
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.ObjectView
18 alias Pleroma.Web.ActivityPub.Relay
19 alias Pleroma.Web.ActivityPub.UserView
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Workers.ReceiverWorker
25 import Pleroma.Factory
27 require Pleroma.Constants
30 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
34 setup do: clear_config([:instance, :federating], true)
37 setup do: clear_config([:instance, :allow_relay])
39 test "with the relay active, it returns the relay user", %{conn: conn} do
42 |> get(activity_pub_path(conn, :relay))
45 assert res["id"] =~ "/relay"
48 test "with the relay disabled, it returns 404", %{conn: conn} do
49 Config.put([:instance, :allow_relay], false)
52 |> get(activity_pub_path(conn, :relay))
56 test "on non-federating instance, it returns 404", %{conn: conn} do
57 Config.put([:instance, :federating], false)
61 |> assign(:user, user)
62 |> get(activity_pub_path(conn, :relay))
67 describe "/internal/fetch" do
68 test "it returns the internal fetch user", %{conn: conn} do
71 |> get(activity_pub_path(conn, :internal_fetch))
74 assert res["id"] =~ "/fetch"
77 test "on non-federating instance, it returns 404", %{conn: conn} do
78 Config.put([:instance, :federating], false)
82 |> assign(:user, user)
83 |> get(activity_pub_path(conn, :internal_fetch))
88 describe "/users/:nickname" do
89 test "it returns a json representation of the user with accept application/json", %{
96 |> put_req_header("accept", "application/json")
97 |> get("/users/#{user.nickname}")
99 user = User.get_cached_by_id(user.id)
101 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
104 test "it returns a json representation of the user with accept application/activity+json", %{
111 |> put_req_header("accept", "application/activity+json")
112 |> get("/users/#{user.nickname}")
114 user = User.get_cached_by_id(user.id)
116 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
119 test "it returns a json representation of the user with accept application/ld+json", %{
128 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
130 |> get("/users/#{user.nickname}")
132 user = User.get_cached_by_id(user.id)
134 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
137 test "it returns 404 for remote users", %{
140 user = insert(:user, local: false, nickname: "remoteuser@example.com")
144 |> put_req_header("accept", "application/json")
145 |> get("/users/#{user.nickname}.json")
147 assert json_response(conn, 404)
150 test "it returns error when user is not found", %{conn: conn} do
153 |> put_req_header("accept", "application/json")
154 |> get("/users/jimm")
155 |> json_response(404)
157 assert response == "Not found"
161 describe "mastodon compatibility routes" do
162 test "it returns a json representation of the object with accept application/json", %{
169 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
170 "actor" => Endpoint.url() <> "/users/raymoo",
171 "to" => [Pleroma.Constants.as_public()]
177 |> put_req_header("accept", "application/json")
178 |> get("/users/raymoo/statuses/999999999")
180 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
183 test "it returns a json representation of the activity with accept application/json", %{
190 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
191 "actor" => Endpoint.url() <> "/users/raymoo",
192 "to" => [Pleroma.Constants.as_public()]
198 "id" => object.data["id"] <> "/activity",
200 "object" => object.data["id"],
201 "actor" => object.data["actor"],
202 "to" => object.data["to"]
204 |> ActivityPub.persist(local: true)
208 |> put_req_header("accept", "application/json")
209 |> get("/users/raymoo/statuses/999999999/activity")
211 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
215 describe "/objects/:uuid" do
216 test "it doesn't return a local-only object", %{conn: conn} do
218 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
220 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
222 object = Object.normalize(post, false)
223 uuid = String.split(object.data["id"], "/") |> List.last()
227 |> put_req_header("accept", "application/json")
228 |> get("/objects/#{uuid}")
230 assert json_response(conn, 404)
233 test "it returns a json representation of the object with accept application/json", %{
237 uuid = String.split(note.data["id"], "/") |> List.last()
241 |> put_req_header("accept", "application/json")
242 |> get("/objects/#{uuid}")
244 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
247 test "it returns a json representation of the object with accept application/activity+json",
250 uuid = String.split(note.data["id"], "/") |> List.last()
254 |> put_req_header("accept", "application/activity+json")
255 |> get("/objects/#{uuid}")
257 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
260 test "it returns a json representation of the object with accept application/ld+json", %{
264 uuid = String.split(note.data["id"], "/") |> List.last()
270 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
272 |> get("/objects/#{uuid}")
274 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
277 test "it returns 404 for non-public messages", %{conn: conn} do
278 note = insert(:direct_note)
279 uuid = String.split(note.data["id"], "/") |> List.last()
283 |> put_req_header("accept", "application/activity+json")
284 |> get("/objects/#{uuid}")
286 assert json_response(conn, 404)
289 test "it returns 404 for tombstone objects", %{conn: conn} do
290 tombstone = insert(:tombstone)
291 uuid = String.split(tombstone.data["id"], "/") |> List.last()
295 |> put_req_header("accept", "application/activity+json")
296 |> get("/objects/#{uuid}")
298 assert json_response(conn, 404)
301 test "it caches a response", %{conn: conn} do
303 uuid = String.split(note.data["id"], "/") |> List.last()
307 |> put_req_header("accept", "application/activity+json")
308 |> get("/objects/#{uuid}")
310 assert json_response(conn1, :ok)
311 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
315 |> put_req_header("accept", "application/activity+json")
316 |> get("/objects/#{uuid}")
318 assert json_response(conn1, :ok) == json_response(conn2, :ok)
319 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
322 test "cached purged after object deletion", %{conn: conn} do
324 uuid = String.split(note.data["id"], "/") |> List.last()
328 |> put_req_header("accept", "application/activity+json")
329 |> get("/objects/#{uuid}")
331 assert json_response(conn1, :ok)
332 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
338 |> put_req_header("accept", "application/activity+json")
339 |> get("/objects/#{uuid}")
341 assert "Not found" == json_response(conn2, :not_found)
345 describe "/activities/:uuid" do
346 test "it doesn't return a local-only activity", %{conn: conn} do
348 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
350 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
352 uuid = String.split(post.data["id"], "/") |> List.last()
356 |> put_req_header("accept", "application/json")
357 |> get("/activities/#{uuid}")
359 assert json_response(conn, 404)
362 test "it returns a json representation of the activity", %{conn: conn} do
363 activity = insert(:note_activity)
364 uuid = String.split(activity.data["id"], "/") |> List.last()
368 |> put_req_header("accept", "application/activity+json")
369 |> get("/activities/#{uuid}")
371 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
374 test "it returns 404 for non-public activities", %{conn: conn} do
375 activity = insert(:direct_note_activity)
376 uuid = String.split(activity.data["id"], "/") |> List.last()
380 |> put_req_header("accept", "application/activity+json")
381 |> get("/activities/#{uuid}")
383 assert json_response(conn, 404)
386 test "it caches a response", %{conn: conn} do
387 activity = insert(:note_activity)
388 uuid = String.split(activity.data["id"], "/") |> List.last()
392 |> put_req_header("accept", "application/activity+json")
393 |> get("/activities/#{uuid}")
395 assert json_response(conn1, :ok)
396 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
400 |> put_req_header("accept", "application/activity+json")
401 |> get("/activities/#{uuid}")
403 assert json_response(conn1, :ok) == json_response(conn2, :ok)
404 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
407 test "cached purged after activity deletion", %{conn: conn} do
409 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
411 uuid = String.split(activity.data["id"], "/") |> List.last()
415 |> put_req_header("accept", "application/activity+json")
416 |> get("/activities/#{uuid}")
418 assert json_response(conn1, :ok)
419 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
421 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
425 |> put_req_header("accept", "application/activity+json")
426 |> get("/activities/#{uuid}")
428 assert "Not found" == json_response(conn2, :not_found)
433 test "it inserts an incoming activity into the database", %{conn: conn} do
434 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
438 |> assign(:valid_signature, true)
439 |> put_req_header("content-type", "application/activity+json")
440 |> post("/inbox", data)
442 assert "ok" == json_response(conn, 200)
444 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
445 assert Activity.get_by_ap_id(data["id"])
448 @tag capture_log: true
449 test "it inserts an incoming activity into the database" <>
450 "even if we can't fetch the user but have it in our db",
454 ap_id: "https://mastodon.example.org/users/raymoo",
457 last_refreshed_at: nil
461 File.read!("test/fixtures/mastodon-post-activity.json")
463 |> Map.put("actor", user.ap_id)
464 |> put_in(["object", "attridbutedTo"], user.ap_id)
468 |> assign(:valid_signature, true)
469 |> put_req_header("content-type", "application/activity+json")
470 |> post("/inbox", data)
472 assert "ok" == json_response(conn, 200)
474 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
475 assert Activity.get_by_ap_id(data["id"])
478 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
479 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
481 sender_url = data["actor"]
482 Instances.set_consistently_unreachable(sender_url)
483 refute Instances.reachable?(sender_url)
487 |> assign(:valid_signature, true)
488 |> put_req_header("content-type", "application/activity+json")
489 |> post("/inbox", data)
491 assert "ok" == json_response(conn, 200)
492 assert Instances.reachable?(sender_url)
495 test "accept follow activity", %{conn: conn} do
496 Pleroma.Config.put([:instance, :federating], true)
497 relay = Relay.get_actor()
499 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
501 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
502 relay = refresh_record(relay)
505 File.read!("test/fixtures/relay/accept-follow.json")
506 |> String.replace("{{ap_id}}", relay.ap_id)
507 |> String.replace("{{activity_id}}", activity.data["id"])
511 |> assign(:valid_signature, true)
512 |> put_req_header("content-type", "application/activity+json")
513 |> post("/inbox", accept)
514 |> json_response(200)
516 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
518 assert Pleroma.FollowingRelationship.following?(
523 Mix.shell(Mix.Shell.Process)
526 Mix.shell(Mix.Shell.IO)
529 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
530 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
533 @tag capture_log: true
534 test "without valid signature, " <>
535 "it only accepts Create activities and requires enabled federation",
537 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
538 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
540 conn = put_req_header(conn, "content-type", "application/activity+json")
542 Config.put([:instance, :federating], false)
545 |> post("/inbox", data)
546 |> json_response(403)
549 |> post("/inbox", non_create_data)
550 |> json_response(403)
552 Config.put([:instance, :federating], true)
554 ret_conn = post(conn, "/inbox", data)
555 assert "ok" == json_response(ret_conn, 200)
558 |> post("/inbox", non_create_data)
559 |> json_response(400)
563 describe "/users/:nickname/inbox" do
566 File.read!("test/fixtures/mastodon-post-activity.json")
572 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
574 data = Map.put(data, "bcc", [user.ap_id])
578 |> assign(:valid_signature, true)
579 |> put_req_header("content-type", "application/activity+json")
580 |> post("/users/#{user.nickname}/inbox", data)
582 assert "ok" == json_response(conn, 200)
583 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
584 assert Activity.get_by_ap_id(data["id"])
587 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
591 Map.put(data, "to", user.ap_id)
596 |> assign(:valid_signature, true)
597 |> put_req_header("content-type", "application/activity+json")
598 |> post("/users/#{user.nickname}/inbox", data)
600 assert "ok" == json_response(conn, 200)
601 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
602 assert Activity.get_by_ap_id(data["id"])
605 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
609 Map.put(data, "cc", user.ap_id)
614 |> assign(:valid_signature, true)
615 |> put_req_header("content-type", "application/activity+json")
616 |> post("/users/#{user.nickname}/inbox", data)
618 assert "ok" == json_response(conn, 200)
619 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
620 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
621 assert user.ap_id in activity.recipients
624 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
628 Map.put(data, "bcc", user.ap_id)
634 |> assign(:valid_signature, true)
635 |> put_req_header("content-type", "application/activity+json")
636 |> post("/users/#{user.nickname}/inbox", data)
638 assert "ok" == json_response(conn, 200)
639 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
640 assert Activity.get_by_ap_id(data["id"])
643 test "it accepts announces with to as string instead of array", %{conn: conn} do
646 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
647 announcer = insert(:user, local: false)
650 "@context" => "https://www.w3.org/ns/activitystreams",
651 "actor" => announcer.ap_id,
652 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
653 "object" => post.data["object"],
654 "to" => "https://www.w3.org/ns/activitystreams#Public",
655 "cc" => [user.ap_id],
661 |> assign(:valid_signature, true)
662 |> put_req_header("content-type", "application/activity+json")
663 |> post("/users/#{user.nickname}/inbox", data)
665 assert "ok" == json_response(conn, 200)
666 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
667 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
668 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
671 test "it accepts messages from actors that are followed by the user", %{
675 recipient = insert(:user)
676 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
678 {:ok, recipient} = User.follow(recipient, actor)
682 |> Map.put("attributedTo", actor.ap_id)
686 |> Map.put("actor", actor.ap_id)
687 |> Map.put("object", object)
691 |> assign(:valid_signature, true)
692 |> put_req_header("content-type", "application/activity+json")
693 |> post("/users/#{recipient.nickname}/inbox", data)
695 assert "ok" == json_response(conn, 200)
696 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
697 assert Activity.get_by_ap_id(data["id"])
700 test "it rejects reads from other users", %{conn: conn} do
702 other_user = insert(:user)
706 |> assign(:user, other_user)
707 |> put_req_header("accept", "application/activity+json")
708 |> get("/users/#{user.nickname}/inbox")
710 assert json_response(conn, 403)
713 test "it returns a note activity in a collection", %{conn: conn} do
714 note_activity = insert(:direct_note_activity)
715 note_object = Object.normalize(note_activity)
716 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
720 |> assign(:user, user)
721 |> put_req_header("accept", "application/activity+json")
722 |> get("/users/#{user.nickname}/inbox?page=true")
724 assert response(conn, 200) =~ note_object.data["content"]
727 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
729 data = Map.put(data, "bcc", [user.ap_id])
731 sender_host = URI.parse(data["actor"]).host
732 Instances.set_consistently_unreachable(sender_host)
733 refute Instances.reachable?(sender_host)
737 |> assign(:valid_signature, true)
738 |> put_req_header("content-type", "application/activity+json")
739 |> post("/users/#{user.nickname}/inbox", data)
741 assert "ok" == json_response(conn, 200)
742 assert Instances.reachable?(sender_host)
745 test "it removes all follower collections but actor's", %{conn: conn} do
746 [actor, recipient] = insert_pair(:user)
749 File.read!("test/fixtures/activitypub-client-post-activity.json")
752 object = Map.put(data["object"], "attributedTo", actor.ap_id)
756 |> Map.put("id", Utils.generate_object_id())
757 |> Map.put("actor", actor.ap_id)
758 |> Map.put("object", object)
760 recipient.follower_address,
761 actor.follower_address
765 recipient.follower_address,
766 "https://www.w3.org/ns/activitystreams#Public"
770 |> assign(:valid_signature, true)
771 |> put_req_header("content-type", "application/activity+json")
772 |> post("/users/#{recipient.nickname}/inbox", data)
773 |> json_response(200)
775 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
777 activity = Activity.get_by_ap_id(data["id"])
780 assert actor.follower_address in activity.recipients
781 assert actor.follower_address in activity.data["cc"]
783 refute recipient.follower_address in activity.recipients
784 refute recipient.follower_address in activity.data["cc"]
785 refute recipient.follower_address in activity.data["to"]
788 test "it requires authentication", %{conn: conn} do
790 conn = put_req_header(conn, "accept", "application/activity+json")
792 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
793 assert json_response(ret_conn, 403)
797 |> assign(:user, user)
798 |> get("/users/#{user.nickname}/inbox")
800 assert json_response(ret_conn, 200)
803 test "forwarded report", %{conn: conn} do
804 admin = insert(:user, is_admin: true)
805 actor = insert(:user, local: false)
806 remote_domain = URI.parse(actor.ap_id).host
807 reported_user = insert(:user)
809 note = insert(:note_activity, user: reported_user)
813 "https://www.w3.org/ns/activitystreams",
814 "https://#{remote_domain}/schemas/litepub-0.1.jsonld",
819 "actor" => actor.ap_id,
824 "context" => "context",
825 "id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
826 "nickname" => reported_user.nickname,
831 "actor_type" => "Person",
832 "approval_pending" => false,
834 "confirmation_pending" => false,
835 "deactivated" => false,
836 "display_name" => "test user",
837 "id" => reported_user.id,
839 "nickname" => reported_user.nickname,
840 "registration_reason" => nil,
846 "url" => reported_user.ap_id
849 "id" => note.data["id"],
850 "published" => note.data["published"],
854 "published" => note.data["published"],
861 |> assign(:valid_signature, true)
862 |> put_req_header("content-type", "application/activity+json")
863 |> post("/users/#{reported_user.nickname}/inbox", data)
864 |> json_response(200)
866 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
868 assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
870 ObanHelpers.perform_all()
872 Swoosh.TestAssertions.assert_email_sent(
873 to: {admin.name, admin.email},
874 html_body: ~r/Reported Account:/i
878 test "forwarded report from mastodon", %{conn: conn} do
879 admin = insert(:user, is_admin: true)
880 actor = insert(:user, local: false)
881 remote_domain = URI.parse(actor.ap_id).host
882 remote_actor = "https://#{remote_domain}/actor"
883 reported_user = insert(:user)
885 note = insert(:note_activity, user: reported_user)
888 "test/fixtures/mastodon/application_actor.json"
890 |> String.replace("{{DOMAIN}}", remote_domain)
892 Tesla.Mock.mock(fn %{url: ^remote_actor} ->
895 body: mock_json_body,
896 headers: [{"content-type", "application/activity+json"}]
901 "@context" => "https://www.w3.org/ns/activitystreams",
902 "actor" => remote_actor,
903 "content" => "test report",
904 "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
905 "nickname" => reported_user.nickname,
914 |> assign(:valid_signature, true)
915 |> put_req_header("content-type", "application/activity+json")
916 |> post("/users/#{reported_user.nickname}/inbox", data)
917 |> json_response(200)
919 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
921 assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
923 flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
924 reported_user_ap_id = reported_user.ap_id
926 [^reported_user_ap_id, flag_data] = flag_activity.data["object"]
928 Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
929 ObanHelpers.perform_all()
931 Swoosh.TestAssertions.assert_email_sent(
932 to: {admin.name, admin.email},
933 html_body: ~r/#{note.data["object"]}/i
938 describe "GET /users/:nickname/outbox" do
939 test "it paginates correctly", %{conn: conn} do
941 conn = assign(conn, :user, user)
942 outbox_endpoint = user.ap_id <> "/outbox"
946 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
952 |> put_req_header("accept", "application/activity+json")
953 |> get(outbox_endpoint <> "?page=true")
954 |> json_response(200)
956 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
957 assert length(result["orderedItems"]) == 20
958 assert length(result_ids) == 20
959 assert result["next"]
960 assert String.starts_with?(result["next"], outbox_endpoint)
964 |> put_req_header("accept", "application/activity+json")
965 |> get(result["next"])
966 |> json_response(200)
968 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
969 assert length(result_next["orderedItems"]) == 6
970 assert length(result_next_ids) == 6
971 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
972 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
973 assert String.starts_with?(result["id"], outbox_endpoint)
977 |> put_req_header("accept", "application/activity+json")
978 |> get(result_next["id"])
979 |> json_response(200)
981 assert result_next == result_next_again
984 test "it returns 200 even if there're no activities", %{conn: conn} do
986 outbox_endpoint = user.ap_id <> "/outbox"
990 |> assign(:user, user)
991 |> put_req_header("accept", "application/activity+json")
992 |> get(outbox_endpoint)
994 result = json_response(conn, 200)
995 assert outbox_endpoint == result["id"]
998 test "it returns a note activity in a collection", %{conn: conn} do
999 note_activity = insert(:note_activity)
1000 note_object = Object.normalize(note_activity)
1001 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1005 |> assign(:user, user)
1006 |> put_req_header("accept", "application/activity+json")
1007 |> get("/users/#{user.nickname}/outbox?page=true")
1009 assert response(conn, 200) =~ note_object.data["content"]
1012 test "it returns an announce activity in a collection", %{conn: conn} do
1013 announce_activity = insert(:announce_activity)
1014 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
1018 |> assign(:user, user)
1019 |> put_req_header("accept", "application/activity+json")
1020 |> get("/users/#{user.nickname}/outbox?page=true")
1022 assert response(conn, 200) =~ announce_activity.data["object"]
1026 describe "POST /users/:nickname/outbox (C2S)" do
1027 setup do: clear_config([:instance, :limit])
1032 "@context" => "https://www.w3.org/ns/activitystreams",
1034 "object" => %{"type" => "Note", "content" => "AP C2S test"},
1035 "to" => "https://www.w3.org/ns/activitystreams#Public",
1041 test "it rejects posts from other users / unauthenticated users", %{
1045 user = insert(:user)
1046 other_user = insert(:user)
1047 conn = put_req_header(conn, "content-type", "application/activity+json")
1050 |> post("/users/#{user.nickname}/outbox", activity)
1051 |> json_response(403)
1054 |> assign(:user, other_user)
1055 |> post("/users/#{user.nickname}/outbox", activity)
1056 |> json_response(403)
1059 test "it inserts an incoming create activity into the database", %{
1063 user = insert(:user)
1067 |> assign(:user, user)
1068 |> put_req_header("content-type", "application/activity+json")
1069 |> post("/users/#{user.nickname}/outbox", activity)
1070 |> json_response(201)
1072 assert Activity.get_by_ap_id(result["id"])
1073 assert result["object"]
1074 assert %Object{data: object} = Object.normalize(result["object"])
1075 assert object["content"] == activity["object"]["content"]
1078 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1079 user = insert(:user)
1083 |> put_in(["object", "type"], "Benis")
1087 |> assign(:user, user)
1088 |> put_req_header("content-type", "application/activity+json")
1089 |> post("/users/#{user.nickname}/outbox", activity)
1090 |> json_response(400)
1093 test "it inserts an incoming sensitive activity into the database", %{
1097 user = insert(:user)
1098 conn = assign(conn, :user, user)
1099 object = Map.put(activity["object"], "sensitive", true)
1100 activity = Map.put(activity, "object", object)
1104 |> put_req_header("content-type", "application/activity+json")
1105 |> post("/users/#{user.nickname}/outbox", activity)
1106 |> json_response(201)
1108 assert Activity.get_by_ap_id(response["id"])
1109 assert response["object"]
1110 assert %Object{data: response_object} = Object.normalize(response["object"])
1111 assert response_object["sensitive"] == true
1112 assert response_object["content"] == activity["object"]["content"]
1116 |> put_req_header("accept", "application/activity+json")
1117 |> get(response["id"])
1118 |> json_response(200)
1120 assert representation["object"]["sensitive"] == true
1123 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1124 user = insert(:user)
1125 activity = Map.put(activity, "type", "BadType")
1129 |> assign(:user, user)
1130 |> put_req_header("content-type", "application/activity+json")
1131 |> post("/users/#{user.nickname}/outbox", activity)
1133 assert json_response(conn, 400)
1136 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1137 note_activity = insert(:note_activity)
1138 note_object = Object.normalize(note_activity)
1139 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1144 id: note_object.data["id"]
1150 |> assign(:user, user)
1151 |> put_req_header("content-type", "application/activity+json")
1152 |> post("/users/#{user.nickname}/outbox", data)
1154 result = json_response(conn, 201)
1155 assert Activity.get_by_ap_id(result["id"])
1157 assert object = Object.get_by_ap_id(note_object.data["id"])
1158 assert object.data["type"] == "Tombstone"
1161 test "it rejects delete activity of object from other actor", %{conn: conn} do
1162 note_activity = insert(:note_activity)
1163 note_object = Object.normalize(note_activity)
1164 user = insert(:user)
1169 id: note_object.data["id"]
1175 |> assign(:user, user)
1176 |> put_req_header("content-type", "application/activity+json")
1177 |> post("/users/#{user.nickname}/outbox", data)
1179 assert json_response(conn, 400)
1182 test "it increases like count when receiving a like action", %{conn: conn} do
1183 note_activity = insert(:note_activity)
1184 note_object = Object.normalize(note_activity)
1185 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1190 id: note_object.data["id"]
1196 |> assign(:user, user)
1197 |> put_req_header("content-type", "application/activity+json")
1198 |> post("/users/#{user.nickname}/outbox", data)
1200 result = json_response(conn, 201)
1201 assert Activity.get_by_ap_id(result["id"])
1203 assert object = Object.get_by_ap_id(note_object.data["id"])
1204 assert object.data["like_count"] == 1
1207 test "it doesn't spreads faulty attributedTo or actor fields", %{
1211 reimu = insert(:user, nickname: "reimu")
1212 cirno = insert(:user, nickname: "cirno")
1219 |> put_in(["object", "actor"], reimu.ap_id)
1220 |> put_in(["object", "attributedTo"], reimu.ap_id)
1221 |> put_in(["actor"], reimu.ap_id)
1222 |> put_in(["attributedTo"], reimu.ap_id)
1226 |> assign(:user, cirno)
1227 |> put_req_header("content-type", "application/activity+json")
1228 |> post("/users/#{reimu.nickname}/outbox", activity)
1229 |> json_response(403)
1233 |> assign(:user, cirno)
1234 |> put_req_header("content-type", "application/activity+json")
1235 |> post("/users/#{cirno.nickname}/outbox", activity)
1236 |> json_response(201)
1238 assert cirno_outbox["attributedTo"] == nil
1239 assert cirno_outbox["actor"] == cirno.ap_id
1241 assert cirno_object = Object.normalize(cirno_outbox["object"])
1242 assert cirno_object.data["actor"] == cirno.ap_id
1243 assert cirno_object.data["attributedTo"] == cirno.ap_id
1246 test "Character limitation", %{conn: conn, activity: activity} do
1247 Pleroma.Config.put([:instance, :limit], 5)
1248 user = insert(:user)
1252 |> assign(:user, user)
1253 |> put_req_header("content-type", "application/activity+json")
1254 |> post("/users/#{user.nickname}/outbox", activity)
1255 |> json_response(400)
1257 assert result == "Note is over the character limit"
1261 describe "/relay/followers" do
1262 test "it returns relay followers", %{conn: conn} do
1263 relay_actor = Relay.get_actor()
1264 user = insert(:user)
1265 User.follow(user, relay_actor)
1269 |> get("/relay/followers")
1270 |> json_response(200)
1272 assert result["first"]["orderedItems"] == [user.ap_id]
1275 test "on non-federating instance, it returns 404", %{conn: conn} do
1276 Config.put([:instance, :federating], false)
1277 user = insert(:user)
1280 |> assign(:user, user)
1281 |> get("/relay/followers")
1282 |> json_response(404)
1286 describe "/relay/following" do
1287 test "it returns relay following", %{conn: conn} do
1290 |> get("/relay/following")
1291 |> json_response(200)
1293 assert result["first"]["orderedItems"] == []
1296 test "on non-federating instance, it returns 404", %{conn: conn} do
1297 Config.put([:instance, :federating], false)
1298 user = insert(:user)
1301 |> assign(:user, user)
1302 |> get("/relay/following")
1303 |> json_response(404)
1307 describe "/users/:nickname/followers" do
1308 test "it returns the followers in a collection", %{conn: conn} do
1309 user = insert(:user)
1310 user_two = insert(:user)
1311 User.follow(user, user_two)
1315 |> assign(:user, user_two)
1316 |> get("/users/#{user_two.nickname}/followers")
1317 |> json_response(200)
1319 assert result["first"]["orderedItems"] == [user.ap_id]
1322 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1323 user = insert(:user)
1324 user_two = insert(:user, hide_followers: true)
1325 User.follow(user, user_two)
1329 |> assign(:user, user)
1330 |> get("/users/#{user_two.nickname}/followers")
1331 |> json_response(200)
1333 assert is_binary(result["first"])
1336 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1338 user = insert(:user)
1339 other_user = insert(:user, hide_followers: true)
1343 |> assign(:user, user)
1344 |> get("/users/#{other_user.nickname}/followers?page=1")
1346 assert result.status == 403
1347 assert result.resp_body == ""
1350 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1352 user = insert(:user, hide_followers: true)
1353 other_user = insert(:user)
1354 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1358 |> assign(:user, user)
1359 |> get("/users/#{user.nickname}/followers?page=1")
1360 |> json_response(200)
1362 assert result["totalItems"] == 1
1363 assert result["orderedItems"] == [other_user.ap_id]
1366 test "it works for more than 10 users", %{conn: conn} do
1367 user = insert(:user)
1369 Enum.each(1..15, fn _ ->
1370 other_user = insert(:user)
1371 User.follow(other_user, user)
1376 |> assign(:user, user)
1377 |> get("/users/#{user.nickname}/followers")
1378 |> json_response(200)
1380 assert length(result["first"]["orderedItems"]) == 10
1381 assert result["first"]["totalItems"] == 15
1382 assert result["totalItems"] == 15
1386 |> assign(:user, user)
1387 |> get("/users/#{user.nickname}/followers?page=2")
1388 |> json_response(200)
1390 assert length(result["orderedItems"]) == 5
1391 assert result["totalItems"] == 15
1394 test "does not require authentication", %{conn: conn} do
1395 user = insert(:user)
1398 |> get("/users/#{user.nickname}/followers")
1399 |> json_response(200)
1403 describe "/users/:nickname/following" do
1404 test "it returns the following in a collection", %{conn: conn} do
1405 user = insert(:user)
1406 user_two = insert(:user)
1407 User.follow(user, user_two)
1411 |> assign(:user, user)
1412 |> get("/users/#{user.nickname}/following")
1413 |> json_response(200)
1415 assert result["first"]["orderedItems"] == [user_two.ap_id]
1418 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1419 user = insert(:user)
1420 user_two = insert(:user, hide_follows: true)
1421 User.follow(user, user_two)
1425 |> assign(:user, user)
1426 |> get("/users/#{user_two.nickname}/following")
1427 |> json_response(200)
1429 assert is_binary(result["first"])
1432 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1434 user = insert(:user)
1435 user_two = insert(:user, hide_follows: true)
1439 |> assign(:user, user)
1440 |> get("/users/#{user_two.nickname}/following?page=1")
1442 assert result.status == 403
1443 assert result.resp_body == ""
1446 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1448 user = insert(:user, hide_follows: true)
1449 other_user = insert(:user)
1450 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1454 |> assign(:user, user)
1455 |> get("/users/#{user.nickname}/following?page=1")
1456 |> json_response(200)
1458 assert result["totalItems"] == 1
1459 assert result["orderedItems"] == [other_user.ap_id]
1462 test "it works for more than 10 users", %{conn: conn} do
1463 user = insert(:user)
1465 Enum.each(1..15, fn _ ->
1466 user = User.get_cached_by_id(user.id)
1467 other_user = insert(:user)
1468 User.follow(user, other_user)
1473 |> assign(:user, user)
1474 |> get("/users/#{user.nickname}/following")
1475 |> json_response(200)
1477 assert length(result["first"]["orderedItems"]) == 10
1478 assert result["first"]["totalItems"] == 15
1479 assert result["totalItems"] == 15
1483 |> assign(:user, user)
1484 |> get("/users/#{user.nickname}/following?page=2")
1485 |> json_response(200)
1487 assert length(result["orderedItems"]) == 5
1488 assert result["totalItems"] == 15
1491 test "does not require authentication", %{conn: conn} do
1492 user = insert(:user)
1495 |> get("/users/#{user.nickname}/following")
1496 |> json_response(200)
1500 describe "delivery tracking" do
1501 test "it tracks a signed object fetch", %{conn: conn} do
1502 user = insert(:user, local: false)
1503 activity = insert(:note_activity)
1504 object = Object.normalize(activity)
1506 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1509 |> put_req_header("accept", "application/activity+json")
1510 |> assign(:user, user)
1512 |> json_response(200)
1514 assert Delivery.get(object.id, user.id)
1517 test "it tracks a signed activity fetch", %{conn: conn} do
1518 user = insert(:user, local: false)
1519 activity = insert(:note_activity)
1520 object = Object.normalize(activity)
1522 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1525 |> put_req_header("accept", "application/activity+json")
1526 |> assign(:user, user)
1527 |> get(activity_path)
1528 |> json_response(200)
1530 assert Delivery.get(object.id, user.id)
1533 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1534 user = insert(:user, local: false)
1535 other_user = insert(:user, local: false)
1536 activity = insert(:note_activity)
1537 object = Object.normalize(activity)
1539 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1542 |> put_req_header("accept", "application/activity+json")
1543 |> assign(:user, user)
1545 |> json_response(200)
1548 |> put_req_header("accept", "application/activity+json")
1549 |> assign(:user, other_user)
1551 |> json_response(200)
1553 assert Delivery.get(object.id, user.id)
1554 assert Delivery.get(object.id, other_user.id)
1557 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1558 user = insert(:user, local: false)
1559 other_user = insert(:user, local: false)
1560 activity = insert(:note_activity)
1561 object = Object.normalize(activity)
1563 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1566 |> put_req_header("accept", "application/activity+json")
1567 |> assign(:user, user)
1568 |> get(activity_path)
1569 |> json_response(200)
1572 |> put_req_header("accept", "application/activity+json")
1573 |> assign(:user, other_user)
1574 |> get(activity_path)
1575 |> json_response(200)
1577 assert Delivery.get(object.id, user.id)
1578 assert Delivery.get(object.id, other_user.id)
1582 describe "Additional ActivityPub C2S endpoints" do
1583 test "GET /api/ap/whoami", %{conn: conn} do
1584 user = insert(:user)
1588 |> assign(:user, user)
1589 |> get("/api/ap/whoami")
1591 user = User.get_cached_by_id(user.id)
1593 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1596 |> get("/api/ap/whoami")
1597 |> json_response(403)
1600 setup do: clear_config([:media_proxy])
1601 setup do: clear_config([Pleroma.Upload])
1603 test "POST /api/ap/upload_media", %{conn: conn} do
1604 user = insert(:user)
1606 desc = "Description of the image"
1608 image = %Plug.Upload{
1609 content_type: "bad/content-type",
1610 path: Path.absname("test/fixtures/image.jpg"),
1611 filename: "an_image.png"
1616 |> assign(:user, user)
1617 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1618 |> json_response(:created)
1620 assert object["name"] == desc
1621 assert object["type"] == "Document"
1622 assert object["actor"] == user.ap_id
1623 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1624 assert is_binary(object_href)
1625 assert object_mediatype == "image/jpeg"
1626 assert String.ends_with?(object_href, ".jpg")
1628 activity_request = %{
1629 "@context" => "https://www.w3.org/ns/activitystreams",
1633 "content" => "AP C2S test, attachment",
1634 "attachment" => [object]
1636 "to" => "https://www.w3.org/ns/activitystreams#Public",
1642 |> assign(:user, user)
1643 |> post("/users/#{user.nickname}/outbox", activity_request)
1644 |> json_response(:created)
1646 assert activity_response["id"]
1647 assert activity_response["object"]
1648 assert activity_response["actor"] == user.ap_id
1650 assert %Object{data: %{"attachment" => [attachment]}} =
1651 Object.normalize(activity_response["object"])
1653 assert attachment["type"] == "Document"
1654 assert attachment["name"] == desc
1658 "href" => ^object_href,
1660 "mediaType" => ^object_mediatype
1662 ] = attachment["url"]
1664 # Fails if unauthenticated
1666 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1667 |> json_response(403)