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 "it returns a json representation of the object with accept application/json", %{
236 uuid = String.split(note.data["id"], "/") |> List.last()
240 |> put_req_header("accept", "application/json")
241 |> get("/objects/#{uuid}")
243 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
246 test "it returns a json representation of the object with accept application/activity+json",
249 uuid = String.split(note.data["id"], "/") |> List.last()
253 |> put_req_header("accept", "application/activity+json")
254 |> get("/objects/#{uuid}")
256 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
259 test "it returns a json representation of the object with accept application/ld+json", %{
263 uuid = String.split(note.data["id"], "/") |> List.last()
269 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
271 |> get("/objects/#{uuid}")
273 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
276 test "it returns 404 for non-public messages", %{conn: conn} do
277 note = insert(:direct_note)
278 uuid = String.split(note.data["id"], "/") |> List.last()
282 |> put_req_header("accept", "application/activity+json")
283 |> get("/objects/#{uuid}")
285 assert json_response(conn, 404)
288 test "it returns 404 for tombstone objects", %{conn: conn} do
289 tombstone = insert(:tombstone)
290 uuid = String.split(tombstone.data["id"], "/") |> List.last()
294 |> put_req_header("accept", "application/activity+json")
295 |> get("/objects/#{uuid}")
297 assert json_response(conn, 404)
300 test "it caches a response", %{conn: conn} do
302 uuid = String.split(note.data["id"], "/") |> List.last()
306 |> put_req_header("accept", "application/activity+json")
307 |> get("/objects/#{uuid}")
309 assert json_response(conn1, :ok)
310 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
314 |> put_req_header("accept", "application/activity+json")
315 |> get("/objects/#{uuid}")
317 assert json_response(conn1, :ok) == json_response(conn2, :ok)
318 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
321 test "cached purged after object deletion", %{conn: conn} do
323 uuid = String.split(note.data["id"], "/") |> List.last()
327 |> put_req_header("accept", "application/activity+json")
328 |> get("/objects/#{uuid}")
330 assert json_response(conn1, :ok)
331 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
337 |> put_req_header("accept", "application/activity+json")
338 |> get("/objects/#{uuid}")
340 assert "Not found" == json_response(conn2, :not_found)
344 describe "/activities/:uuid" do
345 test "it doesn't return a local-only activity", %{conn: conn} do
347 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
349 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
351 uuid = String.split(post.data["id"], "/") |> List.last()
355 |> put_req_header("accept", "application/json")
356 |> get("/activities/#{uuid}")
358 assert json_response(conn, 404)
361 test "it returns a json representation of the activity", %{conn: conn} do
362 activity = insert(:note_activity)
363 uuid = String.split(activity.data["id"], "/") |> List.last()
367 |> put_req_header("accept", "application/activity+json")
368 |> get("/activities/#{uuid}")
370 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
373 test "it returns 404 for non-public activities", %{conn: conn} do
374 activity = insert(:direct_note_activity)
375 uuid = String.split(activity.data["id"], "/") |> List.last()
379 |> put_req_header("accept", "application/activity+json")
380 |> get("/activities/#{uuid}")
382 assert json_response(conn, 404)
385 test "it caches a response", %{conn: conn} do
386 activity = insert(:note_activity)
387 uuid = String.split(activity.data["id"], "/") |> List.last()
391 |> put_req_header("accept", "application/activity+json")
392 |> get("/activities/#{uuid}")
394 assert json_response(conn1, :ok)
395 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
399 |> put_req_header("accept", "application/activity+json")
400 |> get("/activities/#{uuid}")
402 assert json_response(conn1, :ok) == json_response(conn2, :ok)
403 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
406 test "cached purged after activity deletion", %{conn: conn} do
408 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
410 uuid = String.split(activity.data["id"], "/") |> List.last()
414 |> put_req_header("accept", "application/activity+json")
415 |> get("/activities/#{uuid}")
417 assert json_response(conn1, :ok)
418 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
420 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
424 |> put_req_header("accept", "application/activity+json")
425 |> get("/activities/#{uuid}")
427 assert "Not found" == json_response(conn2, :not_found)
432 test "it inserts an incoming activity into the database", %{conn: conn} do
433 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
437 |> assign(:valid_signature, true)
438 |> put_req_header("content-type", "application/activity+json")
439 |> post("/inbox", data)
441 assert "ok" == json_response(conn, 200)
443 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
444 assert Activity.get_by_ap_id(data["id"])
447 @tag capture_log: true
448 test "it inserts an incoming activity into the database" <>
449 "even if we can't fetch the user but have it in our db",
453 ap_id: "https://mastodon.example.org/users/raymoo",
456 last_refreshed_at: nil
460 File.read!("test/fixtures/mastodon-post-activity.json")
462 |> Map.put("actor", user.ap_id)
463 |> put_in(["object", "attridbutedTo"], user.ap_id)
467 |> assign(:valid_signature, true)
468 |> put_req_header("content-type", "application/activity+json")
469 |> post("/inbox", data)
471 assert "ok" == json_response(conn, 200)
473 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
474 assert Activity.get_by_ap_id(data["id"])
477 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
478 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
480 sender_url = data["actor"]
481 Instances.set_consistently_unreachable(sender_url)
482 refute Instances.reachable?(sender_url)
486 |> assign(:valid_signature, true)
487 |> put_req_header("content-type", "application/activity+json")
488 |> post("/inbox", data)
490 assert "ok" == json_response(conn, 200)
491 assert Instances.reachable?(sender_url)
494 test "accept follow activity", %{conn: conn} do
495 clear_config([:instance, :federating], true)
496 relay = Relay.get_actor()
498 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
500 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
501 relay = refresh_record(relay)
504 File.read!("test/fixtures/relay/accept-follow.json")
505 |> String.replace("{{ap_id}}", relay.ap_id)
506 |> String.replace("{{activity_id}}", activity.data["id"])
510 |> assign(:valid_signature, true)
511 |> put_req_header("content-type", "application/activity+json")
512 |> post("/inbox", accept)
513 |> json_response(200)
515 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
517 assert Pleroma.FollowingRelationship.following?(
522 Mix.shell(Mix.Shell.Process)
525 Mix.shell(Mix.Shell.IO)
528 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
529 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
532 @tag capture_log: true
533 test "without valid signature, " <>
534 "it only accepts Create activities and requires enabled federation",
536 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
537 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
539 conn = put_req_header(conn, "content-type", "application/activity+json")
541 clear_config([:instance, :federating], false)
544 |> post("/inbox", data)
545 |> json_response(403)
548 |> post("/inbox", non_create_data)
549 |> json_response(403)
551 clear_config([:instance, :federating], true)
553 ret_conn = post(conn, "/inbox", data)
554 assert "ok" == json_response(ret_conn, 200)
557 |> post("/inbox", non_create_data)
558 |> json_response(400)
562 describe "/users/:nickname/inbox" do
565 File.read!("test/fixtures/mastodon-post-activity.json")
571 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
573 data = Map.put(data, "bcc", [user.ap_id])
577 |> assign(:valid_signature, true)
578 |> put_req_header("content-type", "application/activity+json")
579 |> post("/users/#{user.nickname}/inbox", data)
581 assert "ok" == json_response(conn, 200)
582 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
583 assert Activity.get_by_ap_id(data["id"])
586 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
590 Map.put(data, "to", user.ap_id)
595 |> assign(:valid_signature, true)
596 |> put_req_header("content-type", "application/activity+json")
597 |> post("/users/#{user.nickname}/inbox", data)
599 assert "ok" == json_response(conn, 200)
600 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
601 assert Activity.get_by_ap_id(data["id"])
604 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
608 Map.put(data, "cc", user.ap_id)
613 |> assign(:valid_signature, true)
614 |> put_req_header("content-type", "application/activity+json")
615 |> post("/users/#{user.nickname}/inbox", data)
617 assert "ok" == json_response(conn, 200)
618 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
619 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
620 assert user.ap_id in activity.recipients
623 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
627 Map.put(data, "bcc", user.ap_id)
633 |> assign(:valid_signature, true)
634 |> put_req_header("content-type", "application/activity+json")
635 |> post("/users/#{user.nickname}/inbox", data)
637 assert "ok" == json_response(conn, 200)
638 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
639 assert Activity.get_by_ap_id(data["id"])
642 test "it accepts announces with to as string instead of array", %{conn: conn} do
645 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
646 announcer = insert(:user, local: false)
649 "@context" => "https://www.w3.org/ns/activitystreams",
650 "actor" => announcer.ap_id,
651 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
652 "object" => post.data["object"],
653 "to" => "https://www.w3.org/ns/activitystreams#Public",
654 "cc" => [user.ap_id],
660 |> assign(:valid_signature, true)
661 |> put_req_header("content-type", "application/activity+json")
662 |> post("/users/#{user.nickname}/inbox", data)
664 assert "ok" == json_response(conn, 200)
665 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
666 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
667 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
670 test "it accepts messages from actors that are followed by the user", %{
674 recipient = insert(:user)
675 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
677 {:ok, recipient, actor} = User.follow(recipient, actor)
681 |> Map.put("attributedTo", actor.ap_id)
685 |> Map.put("actor", actor.ap_id)
686 |> Map.put("object", object)
690 |> assign(:valid_signature, true)
691 |> put_req_header("content-type", "application/activity+json")
692 |> post("/users/#{recipient.nickname}/inbox", data)
694 assert "ok" == json_response(conn, 200)
695 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
696 assert Activity.get_by_ap_id(data["id"])
699 test "it rejects reads from other users", %{conn: conn} do
701 other_user = insert(:user)
705 |> assign(:user, other_user)
706 |> put_req_header("accept", "application/activity+json")
707 |> get("/users/#{user.nickname}/inbox")
709 assert json_response(conn, 403)
712 test "it returns a note activity in a collection", %{conn: conn} do
713 note_activity = insert(:direct_note_activity)
714 note_object = Object.normalize(note_activity, fetch: false)
715 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
719 |> assign(:user, user)
720 |> put_req_header("accept", "application/activity+json")
721 |> get("/users/#{user.nickname}/inbox?page=true")
723 assert response(conn, 200) =~ note_object.data["content"]
726 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
728 data = Map.put(data, "bcc", [user.ap_id])
730 sender_host = URI.parse(data["actor"]).host
731 Instances.set_consistently_unreachable(sender_host)
732 refute Instances.reachable?(sender_host)
736 |> assign(:valid_signature, true)
737 |> put_req_header("content-type", "application/activity+json")
738 |> post("/users/#{user.nickname}/inbox", data)
740 assert "ok" == json_response(conn, 200)
741 assert Instances.reachable?(sender_host)
744 test "it removes all follower collections but actor's", %{conn: conn} do
745 [actor, recipient] = insert_pair(:user)
748 File.read!("test/fixtures/activitypub-client-post-activity.json")
751 object = Map.put(data["object"], "attributedTo", actor.ap_id)
755 |> Map.put("id", Utils.generate_object_id())
756 |> Map.put("actor", actor.ap_id)
757 |> Map.put("object", object)
759 recipient.follower_address,
760 actor.follower_address
764 recipient.follower_address,
765 "https://www.w3.org/ns/activitystreams#Public"
769 |> assign(:valid_signature, true)
770 |> put_req_header("content-type", "application/activity+json")
771 |> post("/users/#{recipient.nickname}/inbox", data)
772 |> json_response(200)
774 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
776 activity = Activity.get_by_ap_id(data["id"])
779 assert actor.follower_address in activity.recipients
780 assert actor.follower_address in activity.data["cc"]
782 refute recipient.follower_address in activity.recipients
783 refute recipient.follower_address in activity.data["cc"]
784 refute recipient.follower_address in activity.data["to"]
787 test "it requires authentication", %{conn: conn} do
789 conn = put_req_header(conn, "accept", "application/activity+json")
791 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
792 assert json_response(ret_conn, 403)
796 |> assign(:user, user)
797 |> get("/users/#{user.nickname}/inbox")
799 assert json_response(ret_conn, 200)
802 @tag capture_log: true
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 @tag capture_log: true
879 test "forwarded report from mastodon", %{conn: conn} do
880 admin = insert(:user, is_admin: true)
881 actor = insert(:user, local: false)
882 remote_domain = URI.parse(actor.ap_id).host
883 remote_actor = "https://#{remote_domain}/actor"
884 [reported_user, another] = insert_list(2, :user)
886 note = insert(:note_activity, user: reported_user)
888 Pleroma.Web.CommonAPI.favorite(another, note.id)
891 "test/fixtures/mastodon/application_actor.json"
893 |> String.replace("{{DOMAIN}}", remote_domain)
895 Tesla.Mock.mock(fn %{url: ^remote_actor} ->
898 body: mock_json_body,
899 headers: [{"content-type", "application/activity+json"}]
904 "@context" => "https://www.w3.org/ns/activitystreams",
905 "actor" => remote_actor,
906 "content" => "test report",
907 "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
908 "nickname" => reported_user.nickname,
917 |> assign(:valid_signature, true)
918 |> put_req_header("content-type", "application/activity+json")
919 |> post("/users/#{reported_user.nickname}/inbox", data)
920 |> json_response(200)
922 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
924 flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
925 reported_user_ap_id = reported_user.ap_id
927 [^reported_user_ap_id, flag_data] = flag_activity.data["object"]
929 Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
930 ObanHelpers.perform_all()
932 Swoosh.TestAssertions.assert_email_sent(
933 to: {admin.name, admin.email},
934 html_body: ~r/#{note.data["object"]}/i
939 describe "GET /users/:nickname/outbox" do
940 test "it paginates correctly", %{conn: conn} do
942 conn = assign(conn, :user, user)
943 outbox_endpoint = user.ap_id <> "/outbox"
947 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
953 |> put_req_header("accept", "application/activity+json")
954 |> get(outbox_endpoint <> "?page=true")
955 |> json_response(200)
957 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
958 assert length(result["orderedItems"]) == 20
959 assert length(result_ids) == 20
960 assert result["next"]
961 assert String.starts_with?(result["next"], outbox_endpoint)
965 |> put_req_header("accept", "application/activity+json")
966 |> get(result["next"])
967 |> json_response(200)
969 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
970 assert length(result_next["orderedItems"]) == 6
971 assert length(result_next_ids) == 6
972 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
973 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
974 assert String.starts_with?(result["id"], outbox_endpoint)
978 |> put_req_header("accept", "application/activity+json")
979 |> get(result_next["id"])
980 |> json_response(200)
982 assert result_next == result_next_again
985 test "it returns 200 even if there're no activities", %{conn: conn} do
987 outbox_endpoint = user.ap_id <> "/outbox"
991 |> assign(:user, user)
992 |> put_req_header("accept", "application/activity+json")
993 |> get(outbox_endpoint)
995 result = json_response(conn, 200)
996 assert outbox_endpoint == result["id"]
999 test "it returns a note activity in a collection", %{conn: conn} do
1000 note_activity = insert(:note_activity)
1001 note_object = Object.normalize(note_activity, fetch: false)
1002 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1006 |> assign(:user, user)
1007 |> put_req_header("accept", "application/activity+json")
1008 |> get("/users/#{user.nickname}/outbox?page=true")
1010 assert response(conn, 200) =~ note_object.data["content"]
1013 test "it returns an announce activity in a collection", %{conn: conn} do
1014 announce_activity = insert(:announce_activity)
1015 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
1019 |> assign(:user, user)
1020 |> put_req_header("accept", "application/activity+json")
1021 |> get("/users/#{user.nickname}/outbox?page=true")
1023 assert response(conn, 200) =~ announce_activity.data["object"]
1027 describe "POST /users/:nickname/outbox (C2S)" do
1028 setup do: clear_config([:instance, :limit])
1033 "@context" => "https://www.w3.org/ns/activitystreams",
1035 "object" => %{"type" => "Note", "content" => "AP C2S test"},
1036 "to" => "https://www.w3.org/ns/activitystreams#Public",
1042 test "it rejects posts from other users / unauthenticated users", %{
1046 user = insert(:user)
1047 other_user = insert(:user)
1048 conn = put_req_header(conn, "content-type", "application/activity+json")
1051 |> post("/users/#{user.nickname}/outbox", activity)
1052 |> json_response(403)
1055 |> assign(:user, other_user)
1056 |> post("/users/#{user.nickname}/outbox", activity)
1057 |> json_response(403)
1060 test "it inserts an incoming create activity into the database", %{
1064 user = insert(:user)
1068 |> assign(:user, user)
1069 |> put_req_header("content-type", "application/activity+json")
1070 |> post("/users/#{user.nickname}/outbox", activity)
1071 |> json_response(201)
1073 assert Activity.get_by_ap_id(result["id"])
1074 assert result["object"]
1075 assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
1076 assert object["content"] == activity["object"]["content"]
1079 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1080 user = insert(:user)
1084 |> put_in(["object", "type"], "Benis")
1088 |> assign(:user, user)
1089 |> put_req_header("content-type", "application/activity+json")
1090 |> post("/users/#{user.nickname}/outbox", activity)
1091 |> json_response(400)
1094 test "it inserts an incoming sensitive activity into the database", %{
1098 user = insert(:user)
1099 conn = assign(conn, :user, user)
1100 object = Map.put(activity["object"], "sensitive", true)
1101 activity = Map.put(activity, "object", object)
1105 |> put_req_header("content-type", "application/activity+json")
1106 |> post("/users/#{user.nickname}/outbox", activity)
1107 |> json_response(201)
1109 assert Activity.get_by_ap_id(response["id"])
1110 assert response["object"]
1111 assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false)
1112 assert response_object["sensitive"] == true
1113 assert response_object["content"] == activity["object"]["content"]
1117 |> put_req_header("accept", "application/activity+json")
1118 |> get(response["id"])
1119 |> json_response(200)
1121 assert representation["object"]["sensitive"] == true
1124 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1125 user = insert(:user)
1126 activity = Map.put(activity, "type", "BadType")
1130 |> assign(:user, user)
1131 |> put_req_header("content-type", "application/activity+json")
1132 |> post("/users/#{user.nickname}/outbox", activity)
1134 assert json_response(conn, 400)
1137 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1138 note_activity = insert(:note_activity)
1139 note_object = Object.normalize(note_activity, fetch: false)
1140 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1145 id: note_object.data["id"]
1151 |> assign(:user, user)
1152 |> put_req_header("content-type", "application/activity+json")
1153 |> post("/users/#{user.nickname}/outbox", data)
1155 result = json_response(conn, 201)
1156 assert Activity.get_by_ap_id(result["id"])
1158 assert object = Object.get_by_ap_id(note_object.data["id"])
1159 assert object.data["type"] == "Tombstone"
1162 test "it rejects delete activity of object from other actor", %{conn: conn} do
1163 note_activity = insert(:note_activity)
1164 note_object = Object.normalize(note_activity, fetch: false)
1165 user = insert(:user)
1170 id: note_object.data["id"]
1176 |> assign(:user, user)
1177 |> put_req_header("content-type", "application/activity+json")
1178 |> post("/users/#{user.nickname}/outbox", data)
1180 assert json_response(conn, 400)
1183 test "it increases like count when receiving a like action", %{conn: conn} do
1184 note_activity = insert(:note_activity)
1185 note_object = Object.normalize(note_activity, fetch: false)
1186 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1191 id: note_object.data["id"]
1197 |> assign(:user, user)
1198 |> put_req_header("content-type", "application/activity+json")
1199 |> post("/users/#{user.nickname}/outbox", data)
1201 result = json_response(conn, 201)
1202 assert Activity.get_by_ap_id(result["id"])
1204 assert object = Object.get_by_ap_id(note_object.data["id"])
1205 assert object.data["like_count"] == 1
1208 test "it doesn't spreads faulty attributedTo or actor fields", %{
1212 reimu = insert(:user, nickname: "reimu")
1213 cirno = insert(:user, nickname: "cirno")
1220 |> put_in(["object", "actor"], reimu.ap_id)
1221 |> put_in(["object", "attributedTo"], reimu.ap_id)
1222 |> put_in(["actor"], reimu.ap_id)
1223 |> put_in(["attributedTo"], reimu.ap_id)
1227 |> assign(:user, cirno)
1228 |> put_req_header("content-type", "application/activity+json")
1229 |> post("/users/#{reimu.nickname}/outbox", activity)
1230 |> json_response(403)
1234 |> assign(:user, cirno)
1235 |> put_req_header("content-type", "application/activity+json")
1236 |> post("/users/#{cirno.nickname}/outbox", activity)
1237 |> json_response(201)
1239 assert cirno_outbox["attributedTo"] == nil
1240 assert cirno_outbox["actor"] == cirno.ap_id
1242 assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false)
1243 assert cirno_object.data["actor"] == cirno.ap_id
1244 assert cirno_object.data["attributedTo"] == cirno.ap_id
1247 test "Character limitation", %{conn: conn, activity: activity} do
1248 clear_config([:instance, :limit], 5)
1249 user = insert(:user)
1253 |> assign(:user, user)
1254 |> put_req_header("content-type", "application/activity+json")
1255 |> post("/users/#{user.nickname}/outbox", activity)
1256 |> json_response(400)
1258 assert result == "Note is over the character limit"
1262 describe "/relay/followers" do
1263 test "it returns relay followers", %{conn: conn} do
1264 relay_actor = Relay.get_actor()
1265 user = insert(:user)
1266 User.follow(user, relay_actor)
1270 |> get("/relay/followers")
1271 |> json_response(200)
1273 assert result["first"]["orderedItems"] == [user.ap_id]
1276 test "on non-federating instance, it returns 404", %{conn: conn} do
1277 clear_config([:instance, :federating], false)
1278 user = insert(:user)
1281 |> assign(:user, user)
1282 |> get("/relay/followers")
1283 |> json_response(404)
1287 describe "/relay/following" do
1288 test "it returns relay following", %{conn: conn} do
1291 |> get("/relay/following")
1292 |> json_response(200)
1294 assert result["first"]["orderedItems"] == []
1297 test "on non-federating instance, it returns 404", %{conn: conn} do
1298 clear_config([:instance, :federating], false)
1299 user = insert(:user)
1302 |> assign(:user, user)
1303 |> get("/relay/following")
1304 |> json_response(404)
1308 describe "/users/:nickname/followers" do
1309 test "it returns the followers in a collection", %{conn: conn} do
1310 user = insert(:user)
1311 user_two = insert(:user)
1312 User.follow(user, user_two)
1316 |> assign(:user, user_two)
1317 |> get("/users/#{user_two.nickname}/followers")
1318 |> json_response(200)
1320 assert result["first"]["orderedItems"] == [user.ap_id]
1323 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1324 user = insert(:user)
1325 user_two = insert(:user, hide_followers: true)
1326 User.follow(user, user_two)
1330 |> assign(:user, user)
1331 |> get("/users/#{user_two.nickname}/followers")
1332 |> json_response(200)
1334 assert is_binary(result["first"])
1337 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1339 user = insert(:user)
1340 other_user = insert(:user, hide_followers: true)
1344 |> assign(:user, user)
1345 |> get("/users/#{other_user.nickname}/followers?page=1")
1347 assert result.status == 403
1348 assert result.resp_body == ""
1351 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1353 user = insert(:user, hide_followers: true)
1354 other_user = insert(:user)
1355 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1359 |> assign(:user, user)
1360 |> get("/users/#{user.nickname}/followers?page=1")
1361 |> json_response(200)
1363 assert result["totalItems"] == 1
1364 assert result["orderedItems"] == [other_user.ap_id]
1367 test "it works for more than 10 users", %{conn: conn} do
1368 user = insert(:user)
1370 Enum.each(1..15, fn _ ->
1371 other_user = insert(:user)
1372 User.follow(other_user, user)
1377 |> assign(:user, user)
1378 |> get("/users/#{user.nickname}/followers")
1379 |> json_response(200)
1381 assert length(result["first"]["orderedItems"]) == 10
1382 assert result["first"]["totalItems"] == 15
1383 assert result["totalItems"] == 15
1387 |> assign(:user, user)
1388 |> get("/users/#{user.nickname}/followers?page=2")
1389 |> json_response(200)
1391 assert length(result["orderedItems"]) == 5
1392 assert result["totalItems"] == 15
1395 test "does not require authentication", %{conn: conn} do
1396 user = insert(:user)
1399 |> get("/users/#{user.nickname}/followers")
1400 |> json_response(200)
1404 describe "/users/:nickname/following" do
1405 test "it returns the following in a collection", %{conn: conn} do
1406 user = insert(:user)
1407 user_two = insert(:user)
1408 User.follow(user, user_two)
1412 |> assign(:user, user)
1413 |> get("/users/#{user.nickname}/following")
1414 |> json_response(200)
1416 assert result["first"]["orderedItems"] == [user_two.ap_id]
1419 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1420 user = insert(:user)
1421 user_two = insert(:user, hide_follows: true)
1422 User.follow(user, user_two)
1426 |> assign(:user, user)
1427 |> get("/users/#{user_two.nickname}/following")
1428 |> json_response(200)
1430 assert is_binary(result["first"])
1433 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1435 user = insert(:user)
1436 user_two = insert(:user, hide_follows: true)
1440 |> assign(:user, user)
1441 |> get("/users/#{user_two.nickname}/following?page=1")
1443 assert result.status == 403
1444 assert result.resp_body == ""
1447 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1449 user = insert(:user, hide_follows: true)
1450 other_user = insert(:user)
1451 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1455 |> assign(:user, user)
1456 |> get("/users/#{user.nickname}/following?page=1")
1457 |> json_response(200)
1459 assert result["totalItems"] == 1
1460 assert result["orderedItems"] == [other_user.ap_id]
1463 test "it works for more than 10 users", %{conn: conn} do
1464 user = insert(:user)
1466 Enum.each(1..15, fn _ ->
1467 user = User.get_cached_by_id(user.id)
1468 other_user = insert(:user)
1469 User.follow(user, other_user)
1474 |> assign(:user, user)
1475 |> get("/users/#{user.nickname}/following")
1476 |> json_response(200)
1478 assert length(result["first"]["orderedItems"]) == 10
1479 assert result["first"]["totalItems"] == 15
1480 assert result["totalItems"] == 15
1484 |> assign(:user, user)
1485 |> get("/users/#{user.nickname}/following?page=2")
1486 |> json_response(200)
1488 assert length(result["orderedItems"]) == 5
1489 assert result["totalItems"] == 15
1492 test "does not require authentication", %{conn: conn} do
1493 user = insert(:user)
1496 |> get("/users/#{user.nickname}/following")
1497 |> json_response(200)
1501 describe "delivery tracking" do
1502 test "it tracks a signed object fetch", %{conn: conn} do
1503 user = insert(:user, local: false)
1504 activity = insert(:note_activity)
1505 object = Object.normalize(activity, fetch: false)
1507 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1510 |> put_req_header("accept", "application/activity+json")
1511 |> assign(:user, user)
1513 |> json_response(200)
1515 assert Delivery.get(object.id, user.id)
1518 test "it tracks a signed activity fetch", %{conn: conn} do
1519 user = insert(:user, local: false)
1520 activity = insert(:note_activity)
1521 object = Object.normalize(activity, fetch: false)
1523 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1526 |> put_req_header("accept", "application/activity+json")
1527 |> assign(:user, user)
1528 |> get(activity_path)
1529 |> json_response(200)
1531 assert Delivery.get(object.id, user.id)
1534 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1535 user = insert(:user, local: false)
1536 other_user = insert(:user, local: false)
1537 activity = insert(:note_activity)
1538 object = Object.normalize(activity, fetch: false)
1540 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1543 |> put_req_header("accept", "application/activity+json")
1544 |> assign(:user, user)
1546 |> json_response(200)
1549 |> put_req_header("accept", "application/activity+json")
1550 |> assign(:user, other_user)
1552 |> json_response(200)
1554 assert Delivery.get(object.id, user.id)
1555 assert Delivery.get(object.id, other_user.id)
1558 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1559 user = insert(:user, local: false)
1560 other_user = insert(:user, local: false)
1561 activity = insert(:note_activity)
1562 object = Object.normalize(activity, fetch: false)
1564 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1567 |> put_req_header("accept", "application/activity+json")
1568 |> assign(:user, user)
1569 |> get(activity_path)
1570 |> json_response(200)
1573 |> put_req_header("accept", "application/activity+json")
1574 |> assign(:user, other_user)
1575 |> get(activity_path)
1576 |> json_response(200)
1578 assert Delivery.get(object.id, user.id)
1579 assert Delivery.get(object.id, other_user.id)
1583 describe "Additional ActivityPub C2S endpoints" do
1584 test "GET /api/ap/whoami", %{conn: conn} do
1585 user = insert(:user)
1589 |> assign(:user, user)
1590 |> get("/api/ap/whoami")
1592 user = User.get_cached_by_id(user.id)
1594 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1597 |> get("/api/ap/whoami")
1598 |> json_response(403)
1601 setup do: clear_config([:media_proxy])
1602 setup do: clear_config([Pleroma.Upload])
1604 test "POST /api/ap/upload_media", %{conn: conn} do
1605 user = insert(:user)
1607 desc = "Description of the image"
1609 image = %Plug.Upload{
1610 content_type: "image/jpeg",
1611 path: Path.absname("test/fixtures/image.jpg"),
1612 filename: "an_image.jpg"
1617 |> assign(:user, user)
1618 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1619 |> json_response(:created)
1621 assert object["name"] == desc
1622 assert object["type"] == "Document"
1623 assert object["actor"] == user.ap_id
1624 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1625 assert is_binary(object_href)
1626 assert object_mediatype == "image/jpeg"
1627 assert String.ends_with?(object_href, ".jpg")
1629 activity_request = %{
1630 "@context" => "https://www.w3.org/ns/activitystreams",
1634 "content" => "AP C2S test, attachment",
1635 "attachment" => [object]
1637 "to" => "https://www.w3.org/ns/activitystreams#Public",
1643 |> assign(:user, user)
1644 |> post("/users/#{user.nickname}/outbox", activity_request)
1645 |> json_response(:created)
1647 assert activity_response["id"]
1648 assert activity_response["object"]
1649 assert activity_response["actor"] == user.ap_id
1651 assert %Object{data: %{"attachment" => [attachment]}} =
1652 Object.normalize(activity_response["object"], fetch: false)
1654 assert attachment["type"] == "Document"
1655 assert attachment["name"] == desc
1659 "href" => ^object_href,
1661 "mediaType" => ^object_mediatype
1663 ] = attachment["url"]
1665 # Fails if unauthenticated
1667 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1668 |> json_response(403)