1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 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.Activity
11 alias Pleroma.Instances
13 alias Pleroma.Tests.ObanHelpers
15 alias Pleroma.Web.ActivityPub.ObjectView
16 alias Pleroma.Web.ActivityPub.Relay
17 alias Pleroma.Web.ActivityPub.UserView
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Workers.ReceiverWorker
23 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
27 clear_config_all([:instance, :federating],
28 do: Pleroma.Config.put([:instance, :federating], true)
32 clear_config([:instance, :allow_relay])
34 test "with the relay active, it returns the relay user", %{conn: conn} do
37 |> get(activity_pub_path(conn, :relay))
40 assert res["id"] =~ "/relay"
43 test "with the relay disabled, it returns 404", %{conn: conn} do
44 Pleroma.Config.put([:instance, :allow_relay], false)
47 |> get(activity_pub_path(conn, :relay))
53 describe "/internal/fetch" do
54 test "it returns the internal fetch user", %{conn: conn} do
57 |> get(activity_pub_path(conn, :internal_fetch))
60 assert res["id"] =~ "/fetch"
64 describe "/users/:nickname" do
65 test "it returns a json representation of the user with accept application/json", %{
72 |> put_req_header("accept", "application/json")
73 |> get("/users/#{user.nickname}")
75 user = User.get_cached_by_id(user.id)
77 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
80 test "it returns a json representation of the user with accept application/activity+json", %{
87 |> put_req_header("accept", "application/activity+json")
88 |> get("/users/#{user.nickname}")
90 user = User.get_cached_by_id(user.id)
92 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
95 test "it returns a json representation of the user with accept application/ld+json", %{
104 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
106 |> get("/users/#{user.nickname}")
108 user = User.get_cached_by_id(user.id)
110 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
114 describe "/object/:uuid" do
115 test "it returns a json representation of the object with accept application/json", %{
119 uuid = String.split(note.data["id"], "/") |> List.last()
123 |> put_req_header("accept", "application/json")
124 |> get("/objects/#{uuid}")
126 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
129 test "it returns a json representation of the object with accept application/activity+json",
132 uuid = String.split(note.data["id"], "/") |> List.last()
136 |> put_req_header("accept", "application/activity+json")
137 |> get("/objects/#{uuid}")
139 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
142 test "it returns a json representation of the object with accept application/ld+json", %{
146 uuid = String.split(note.data["id"], "/") |> List.last()
152 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
154 |> get("/objects/#{uuid}")
156 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
159 test "it returns 404 for non-public messages", %{conn: conn} do
160 note = insert(:direct_note)
161 uuid = String.split(note.data["id"], "/") |> List.last()
165 |> put_req_header("accept", "application/activity+json")
166 |> get("/objects/#{uuid}")
168 assert json_response(conn, 404)
171 test "it returns 404 for tombstone objects", %{conn: conn} do
172 tombstone = insert(:tombstone)
173 uuid = String.split(tombstone.data["id"], "/") |> List.last()
177 |> put_req_header("accept", "application/activity+json")
178 |> get("/objects/#{uuid}")
180 assert json_response(conn, 404)
184 describe "/object/:uuid/likes" do
186 like = insert(:like_activity)
187 like_object_ap_id = Object.normalize(like).data["id"]
194 [id: like.data["id"], uuid: uuid]
197 test "it returns the like activities in a collection", %{conn: conn, id: id, uuid: uuid} do
200 |> put_req_header("accept", "application/activity+json")
201 |> get("/objects/#{uuid}/likes")
202 |> json_response(200)
204 assert List.first(result["first"]["orderedItems"])["id"] == id
205 assert result["type"] == "OrderedCollection"
206 assert result["totalItems"] == 1
207 refute result["first"]["next"]
210 test "it does not crash when page number is exceeded total pages", %{conn: conn, uuid: uuid} do
213 |> put_req_header("accept", "application/activity+json")
214 |> get("/objects/#{uuid}/likes?page=2")
215 |> json_response(200)
217 assert result["type"] == "OrderedCollectionPage"
218 assert result["totalItems"] == 1
219 refute result["next"]
220 assert Enum.empty?(result["orderedItems"])
223 test "it contains the next key when likes count is more than 10", %{conn: conn} do
224 note = insert(:note_activity)
225 insert_list(11, :like_activity, note_activity: note)
229 |> Object.normalize()
237 |> put_req_header("accept", "application/activity+json")
238 |> get("/objects/#{uuid}/likes?page=1")
239 |> json_response(200)
241 assert result["totalItems"] == 11
242 assert length(result["orderedItems"]) == 10
243 assert result["next"]
247 describe "/activities/:uuid" do
248 test "it returns a json representation of the activity", %{conn: conn} do
249 activity = insert(:note_activity)
250 uuid = String.split(activity.data["id"], "/") |> List.last()
254 |> put_req_header("accept", "application/activity+json")
255 |> get("/activities/#{uuid}")
257 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
260 test "it returns 404 for non-public activities", %{conn: conn} do
261 activity = insert(:direct_note_activity)
262 uuid = String.split(activity.data["id"], "/") |> List.last()
266 |> put_req_header("accept", "application/activity+json")
267 |> get("/activities/#{uuid}")
269 assert json_response(conn, 404)
274 test "it inserts an incoming activity into the database", %{conn: conn} do
275 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
279 |> assign(:valid_signature, true)
280 |> put_req_header("content-type", "application/activity+json")
281 |> post("/inbox", data)
283 assert "ok" == json_response(conn, 200)
285 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
286 assert Activity.get_by_ap_id(data["id"])
289 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
290 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
292 sender_url = data["actor"]
293 Instances.set_consistently_unreachable(sender_url)
294 refute Instances.reachable?(sender_url)
298 |> assign(:valid_signature, true)
299 |> put_req_header("content-type", "application/activity+json")
300 |> post("/inbox", data)
302 assert "ok" == json_response(conn, 200)
303 assert Instances.reachable?(sender_url)
307 describe "/users/:nickname/inbox" do
310 File.read!("test/fixtures/mastodon-post-activity.json")
316 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
318 data = Map.put(data, "bcc", [user.ap_id])
322 |> assign(:valid_signature, true)
323 |> put_req_header("content-type", "application/activity+json")
324 |> post("/users/#{user.nickname}/inbox", data)
326 assert "ok" == json_response(conn, 200)
327 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
328 assert Activity.get_by_ap_id(data["id"])
331 test "it accepts messages from actors that are followed by the user", %{
335 recipient = insert(:user)
336 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
338 {:ok, recipient} = User.follow(recipient, actor)
342 |> Map.put("attributedTo", actor.ap_id)
346 |> Map.put("actor", actor.ap_id)
347 |> Map.put("object", object)
351 |> assign(:valid_signature, true)
352 |> put_req_header("content-type", "application/activity+json")
353 |> post("/users/#{recipient.nickname}/inbox", data)
355 assert "ok" == json_response(conn, 200)
356 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
357 assert Activity.get_by_ap_id(data["id"])
360 test "it rejects reads from other users", %{conn: conn} do
362 otheruser = insert(:user)
366 |> assign(:user, otheruser)
367 |> put_req_header("accept", "application/activity+json")
368 |> get("/users/#{user.nickname}/inbox")
370 assert json_response(conn, 403)
373 test "it returns a note activity in a collection", %{conn: conn} do
374 note_activity = insert(:direct_note_activity)
375 note_object = Object.normalize(note_activity)
376 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
380 |> assign(:user, user)
381 |> put_req_header("accept", "application/activity+json")
382 |> get("/users/#{user.nickname}/inbox")
384 assert response(conn, 200) =~ note_object.data["content"]
387 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
389 data = Map.put(data, "bcc", [user.ap_id])
391 sender_host = URI.parse(data["actor"]).host
392 Instances.set_consistently_unreachable(sender_host)
393 refute Instances.reachable?(sender_host)
397 |> assign(:valid_signature, true)
398 |> put_req_header("content-type", "application/activity+json")
399 |> post("/users/#{user.nickname}/inbox", data)
401 assert "ok" == json_response(conn, 200)
402 assert Instances.reachable?(sender_host)
405 test "it removes all follower collections but actor's", %{conn: conn} do
406 [actor, recipient] = insert_pair(:user)
409 File.read!("test/fixtures/activitypub-client-post-activity.json")
412 object = Map.put(data["object"], "attributedTo", actor.ap_id)
416 |> Map.put("id", Utils.generate_object_id())
417 |> Map.put("actor", actor.ap_id)
418 |> Map.put("object", object)
420 recipient.follower_address,
421 actor.follower_address
425 recipient.follower_address,
426 "https://www.w3.org/ns/activitystreams#Public"
430 |> assign(:valid_signature, true)
431 |> put_req_header("content-type", "application/activity+json")
432 |> post("/users/#{recipient.nickname}/inbox", data)
433 |> json_response(200)
435 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
437 activity = Activity.get_by_ap_id(data["id"])
440 assert actor.follower_address in activity.recipients
441 assert actor.follower_address in activity.data["cc"]
443 refute recipient.follower_address in activity.recipients
444 refute recipient.follower_address in activity.data["cc"]
445 refute recipient.follower_address in activity.data["to"]
449 describe "/users/:nickname/outbox" do
450 test "it will not bomb when there is no activity", %{conn: conn} do
455 |> put_req_header("accept", "application/activity+json")
456 |> get("/users/#{user.nickname}/outbox")
458 result = json_response(conn, 200)
459 assert user.ap_id <> "/outbox" == result["id"]
462 test "it returns a note activity in a collection", %{conn: conn} do
463 note_activity = insert(:note_activity)
464 note_object = Object.normalize(note_activity)
465 user = User.get_cached_by_ap_id(note_activity.data["actor"])
469 |> put_req_header("accept", "application/activity+json")
470 |> get("/users/#{user.nickname}/outbox")
472 assert response(conn, 200) =~ note_object.data["content"]
475 test "it returns an announce activity in a collection", %{conn: conn} do
476 announce_activity = insert(:announce_activity)
477 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
481 |> put_req_header("accept", "application/activity+json")
482 |> get("/users/#{user.nickname}/outbox")
484 assert response(conn, 200) =~ announce_activity.data["object"]
487 test "it rejects posts from other users", %{conn: conn} do
488 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
490 otheruser = insert(:user)
494 |> assign(:user, otheruser)
495 |> put_req_header("content-type", "application/activity+json")
496 |> post("/users/#{user.nickname}/outbox", data)
498 assert json_response(conn, 403)
501 test "it inserts an incoming create activity into the database", %{conn: conn} do
502 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
507 |> assign(:user, user)
508 |> put_req_header("content-type", "application/activity+json")
509 |> post("/users/#{user.nickname}/outbox", data)
511 result = json_response(conn, 201)
513 assert Activity.get_by_ap_id(result["id"])
516 test "it rejects an incoming activity with bogus type", %{conn: conn} do
517 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
522 |> Map.put("type", "BadType")
526 |> assign(:user, user)
527 |> put_req_header("content-type", "application/activity+json")
528 |> post("/users/#{user.nickname}/outbox", data)
530 assert json_response(conn, 400)
533 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
534 note_activity = insert(:note_activity)
535 note_object = Object.normalize(note_activity)
536 user = User.get_cached_by_ap_id(note_activity.data["actor"])
541 id: note_object.data["id"]
547 |> assign(:user, user)
548 |> put_req_header("content-type", "application/activity+json")
549 |> post("/users/#{user.nickname}/outbox", data)
551 result = json_response(conn, 201)
552 assert Activity.get_by_ap_id(result["id"])
554 assert object = Object.get_by_ap_id(note_object.data["id"])
555 assert object.data["type"] == "Tombstone"
558 test "it rejects delete activity of object from other actor", %{conn: conn} do
559 note_activity = insert(:note_activity)
560 note_object = Object.normalize(note_activity)
566 id: note_object.data["id"]
572 |> assign(:user, user)
573 |> put_req_header("content-type", "application/activity+json")
574 |> post("/users/#{user.nickname}/outbox", data)
576 assert json_response(conn, 400)
579 test "it increases like count when receiving a like action", %{conn: conn} do
580 note_activity = insert(:note_activity)
581 note_object = Object.normalize(note_activity)
582 user = User.get_cached_by_ap_id(note_activity.data["actor"])
587 id: note_object.data["id"]
593 |> assign(:user, user)
594 |> put_req_header("content-type", "application/activity+json")
595 |> post("/users/#{user.nickname}/outbox", data)
597 result = json_response(conn, 201)
598 assert Activity.get_by_ap_id(result["id"])
600 assert object = Object.get_by_ap_id(note_object.data["id"])
601 assert object.data["like_count"] == 1
605 describe "/relay/followers" do
606 test "it returns relay followers", %{conn: conn} do
607 relay_actor = Relay.get_actor()
609 User.follow(user, relay_actor)
613 |> assign(:relay, true)
614 |> get("/relay/followers")
615 |> json_response(200)
617 assert result["first"]["orderedItems"] == [user.ap_id]
621 describe "/relay/following" do
622 test "it returns relay following", %{conn: conn} do
625 |> assign(:relay, true)
626 |> get("/relay/following")
627 |> json_response(200)
629 assert result["first"]["orderedItems"] == []
633 describe "/users/:nickname/followers" do
634 test "it returns the followers in a collection", %{conn: conn} do
636 user_two = insert(:user)
637 User.follow(user, user_two)
641 |> get("/users/#{user_two.nickname}/followers")
642 |> json_response(200)
644 assert result["first"]["orderedItems"] == [user.ap_id]
647 test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
649 user_two = insert(:user, %{info: %{hide_followers: true}})
650 User.follow(user, user_two)
654 |> get("/users/#{user_two.nickname}/followers")
655 |> json_response(200)
657 assert is_binary(result["first"])
660 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
662 user = insert(:user, %{info: %{hide_followers: true}})
666 |> get("/users/#{user.nickname}/followers?page=1")
668 assert result.status == 403
669 assert result.resp_body == ""
672 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
674 user = insert(:user, %{info: %{hide_followers: true}})
675 other_user = insert(:user)
676 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
680 |> assign(:user, user)
681 |> get("/users/#{user.nickname}/followers?page=1")
682 |> json_response(200)
684 assert result["totalItems"] == 1
685 assert result["orderedItems"] == [other_user.ap_id]
688 test "it works for more than 10 users", %{conn: conn} do
691 Enum.each(1..15, fn _ ->
692 other_user = insert(:user)
693 User.follow(other_user, user)
698 |> get("/users/#{user.nickname}/followers")
699 |> json_response(200)
701 assert length(result["first"]["orderedItems"]) == 10
702 assert result["first"]["totalItems"] == 15
703 assert result["totalItems"] == 15
707 |> get("/users/#{user.nickname}/followers?page=2")
708 |> json_response(200)
710 assert length(result["orderedItems"]) == 5
711 assert result["totalItems"] == 15
715 describe "/users/:nickname/following" do
716 test "it returns the following in a collection", %{conn: conn} do
718 user_two = insert(:user)
719 User.follow(user, user_two)
723 |> get("/users/#{user.nickname}/following")
724 |> json_response(200)
726 assert result["first"]["orderedItems"] == [user_two.ap_id]
729 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
730 user = insert(:user, %{info: %{hide_follows: true}})
731 user_two = insert(:user)
732 User.follow(user, user_two)
736 |> get("/users/#{user.nickname}/following")
737 |> json_response(200)
739 assert is_binary(result["first"])
742 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
744 user = insert(:user, %{info: %{hide_follows: true}})
748 |> get("/users/#{user.nickname}/following?page=1")
750 assert result.status == 403
751 assert result.resp_body == ""
754 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
756 user = insert(:user, %{info: %{hide_follows: true}})
757 other_user = insert(:user)
758 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
762 |> assign(:user, user)
763 |> get("/users/#{user.nickname}/following?page=1")
764 |> json_response(200)
766 assert result["totalItems"] == 1
767 assert result["orderedItems"] == [other_user.ap_id]
770 test "it works for more than 10 users", %{conn: conn} do
773 Enum.each(1..15, fn _ ->
774 user = User.get_cached_by_id(user.id)
775 other_user = insert(:user)
776 User.follow(user, other_user)
781 |> get("/users/#{user.nickname}/following")
782 |> json_response(200)
784 assert length(result["first"]["orderedItems"]) == 10
785 assert result["first"]["totalItems"] == 15
786 assert result["totalItems"] == 15
790 |> get("/users/#{user.nickname}/following?page=2")
791 |> json_response(200)
793 assert length(result["orderedItems"]) == 5
794 assert result["totalItems"] == 15