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.UserView
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Workers.Receiver, as: ReceiverWorker
22 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
26 clear_config_all([:instance, :federating],
27 do: Pleroma.Config.put([:instance, :federating], true)
31 clear_config([:instance, :allow_relay])
33 test "with the relay active, it returns the relay user", %{conn: conn} do
36 |> get(activity_pub_path(conn, :relay))
39 assert res["id"] =~ "/relay"
42 test "with the relay disabled, it returns 404", %{conn: conn} do
43 Pleroma.Config.put([:instance, :allow_relay], false)
46 |> get(activity_pub_path(conn, :relay))
52 describe "/internal/fetch" do
53 test "it returns the internal fetch user", %{conn: conn} do
56 |> get(activity_pub_path(conn, :internal_fetch))
59 assert res["id"] =~ "/fetch"
63 describe "/users/:nickname" do
64 test "it returns a json representation of the user with accept application/json", %{
71 |> put_req_header("accept", "application/json")
72 |> get("/users/#{user.nickname}")
74 user = User.get_cached_by_id(user.id)
76 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
79 test "it returns a json representation of the user with accept application/activity+json", %{
86 |> put_req_header("accept", "application/activity+json")
87 |> get("/users/#{user.nickname}")
89 user = User.get_cached_by_id(user.id)
91 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
94 test "it returns a json representation of the user with accept application/ld+json", %{
103 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
105 |> get("/users/#{user.nickname}")
107 user = User.get_cached_by_id(user.id)
109 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
113 describe "/object/:uuid" do
114 test "it returns a json representation of the object with accept application/json", %{
118 uuid = String.split(note.data["id"], "/") |> List.last()
122 |> put_req_header("accept", "application/json")
123 |> get("/objects/#{uuid}")
125 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
128 test "it returns a json representation of the object with accept application/activity+json",
131 uuid = String.split(note.data["id"], "/") |> List.last()
135 |> put_req_header("accept", "application/activity+json")
136 |> get("/objects/#{uuid}")
138 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
141 test "it returns a json representation of the object with accept application/ld+json", %{
145 uuid = String.split(note.data["id"], "/") |> List.last()
151 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
153 |> get("/objects/#{uuid}")
155 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
158 test "it returns 404 for non-public messages", %{conn: conn} do
159 note = insert(:direct_note)
160 uuid = String.split(note.data["id"], "/") |> List.last()
164 |> put_req_header("accept", "application/activity+json")
165 |> get("/objects/#{uuid}")
167 assert json_response(conn, 404)
170 test "it returns 404 for tombstone objects", %{conn: conn} do
171 tombstone = insert(:tombstone)
172 uuid = String.split(tombstone.data["id"], "/") |> List.last()
176 |> put_req_header("accept", "application/activity+json")
177 |> get("/objects/#{uuid}")
179 assert json_response(conn, 404)
183 describe "/object/:uuid/likes" do
185 like = insert(:like_activity)
186 like_object_ap_id = Object.normalize(like).data["id"]
193 [id: like.data["id"], uuid: uuid]
196 test "it returns the like activities in a collection", %{conn: conn, id: id, uuid: uuid} do
199 |> put_req_header("accept", "application/activity+json")
200 |> get("/objects/#{uuid}/likes")
201 |> json_response(200)
203 assert List.first(result["first"]["orderedItems"])["id"] == id
204 assert result["type"] == "OrderedCollection"
205 assert result["totalItems"] == 1
206 refute result["first"]["next"]
209 test "it does not crash when page number is exceeded total pages", %{conn: conn, uuid: uuid} do
212 |> put_req_header("accept", "application/activity+json")
213 |> get("/objects/#{uuid}/likes?page=2")
214 |> json_response(200)
216 assert result["type"] == "OrderedCollectionPage"
217 assert result["totalItems"] == 1
218 refute result["next"]
219 assert Enum.empty?(result["orderedItems"])
222 test "it contains the next key when likes count is more than 10", %{conn: conn} do
223 note = insert(:note_activity)
224 insert_list(11, :like_activity, note_activity: note)
228 |> Object.normalize()
236 |> put_req_header("accept", "application/activity+json")
237 |> get("/objects/#{uuid}/likes?page=1")
238 |> json_response(200)
240 assert result["totalItems"] == 11
241 assert length(result["orderedItems"]) == 10
242 assert result["next"]
246 describe "/activities/:uuid" do
247 test "it returns a json representation of the activity", %{conn: conn} do
248 activity = insert(:note_activity)
249 uuid = String.split(activity.data["id"], "/") |> List.last()
253 |> put_req_header("accept", "application/activity+json")
254 |> get("/activities/#{uuid}")
256 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
259 test "it returns 404 for non-public activities", %{conn: conn} do
260 activity = insert(:direct_note_activity)
261 uuid = String.split(activity.data["id"], "/") |> List.last()
265 |> put_req_header("accept", "application/activity+json")
266 |> get("/activities/#{uuid}")
268 assert json_response(conn, 404)
273 test "it inserts an incoming activity into the database", %{conn: conn} do
274 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
278 |> assign(:valid_signature, true)
279 |> put_req_header("content-type", "application/activity+json")
280 |> post("/inbox", data)
282 assert "ok" == json_response(conn, 200)
284 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
285 assert Activity.get_by_ap_id(data["id"])
288 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
289 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
291 sender_url = data["actor"]
292 Instances.set_consistently_unreachable(sender_url)
293 refute Instances.reachable?(sender_url)
297 |> assign(:valid_signature, true)
298 |> put_req_header("content-type", "application/activity+json")
299 |> post("/inbox", data)
301 assert "ok" == json_response(conn, 200)
302 assert Instances.reachable?(sender_url)
306 describe "/users/:nickname/inbox" do
309 File.read!("test/fixtures/mastodon-post-activity.json")
315 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
317 data = Map.put(data, "bcc", [user.ap_id])
321 |> assign(:valid_signature, true)
322 |> put_req_header("content-type", "application/activity+json")
323 |> post("/users/#{user.nickname}/inbox", data)
325 assert "ok" == json_response(conn, 200)
326 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
327 assert Activity.get_by_ap_id(data["id"])
330 test "it accepts messages from actors that are followed by the user", %{
334 recipient = insert(:user)
335 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
337 {:ok, recipient} = User.follow(recipient, actor)
341 |> Map.put("attributedTo", actor.ap_id)
345 |> Map.put("actor", actor.ap_id)
346 |> Map.put("object", object)
350 |> assign(:valid_signature, true)
351 |> put_req_header("content-type", "application/activity+json")
352 |> post("/users/#{recipient.nickname}/inbox", data)
354 assert "ok" == json_response(conn, 200)
355 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
356 assert Activity.get_by_ap_id(data["id"])
359 test "it rejects reads from other users", %{conn: conn} do
361 otheruser = insert(:user)
365 |> assign(:user, otheruser)
366 |> put_req_header("accept", "application/activity+json")
367 |> get("/users/#{user.nickname}/inbox")
369 assert json_response(conn, 403)
372 test "it returns a note activity in a collection", %{conn: conn} do
373 note_activity = insert(:direct_note_activity)
374 note_object = Object.normalize(note_activity)
375 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
379 |> assign(:user, user)
380 |> put_req_header("accept", "application/activity+json")
381 |> get("/users/#{user.nickname}/inbox")
383 assert response(conn, 200) =~ note_object.data["content"]
386 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
388 data = Map.put(data, "bcc", [user.ap_id])
390 sender_host = URI.parse(data["actor"]).host
391 Instances.set_consistently_unreachable(sender_host)
392 refute Instances.reachable?(sender_host)
396 |> assign(:valid_signature, true)
397 |> put_req_header("content-type", "application/activity+json")
398 |> post("/users/#{user.nickname}/inbox", data)
400 assert "ok" == json_response(conn, 200)
401 assert Instances.reachable?(sender_host)
404 test "it removes all follower collections but actor's", %{conn: conn} do
405 [actor, recipient] = insert_pair(:user)
408 File.read!("test/fixtures/activitypub-client-post-activity.json")
411 object = Map.put(data["object"], "attributedTo", actor.ap_id)
415 |> Map.put("id", Utils.generate_object_id())
416 |> Map.put("actor", actor.ap_id)
417 |> Map.put("object", object)
419 recipient.follower_address,
420 actor.follower_address
424 recipient.follower_address,
425 "https://www.w3.org/ns/activitystreams#Public"
429 |> assign(:valid_signature, true)
430 |> put_req_header("content-type", "application/activity+json")
431 |> post("/users/#{recipient.nickname}/inbox", data)
432 |> json_response(200)
434 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
436 activity = Activity.get_by_ap_id(data["id"])
439 assert actor.follower_address in activity.recipients
440 assert actor.follower_address in activity.data["cc"]
442 refute recipient.follower_address in activity.recipients
443 refute recipient.follower_address in activity.data["cc"]
444 refute recipient.follower_address in activity.data["to"]
448 describe "/users/:nickname/outbox" do
449 test "it will not bomb when there is no activity", %{conn: conn} do
454 |> put_req_header("accept", "application/activity+json")
455 |> get("/users/#{user.nickname}/outbox")
457 result = json_response(conn, 200)
458 assert user.ap_id <> "/outbox" == result["id"]
461 test "it returns a note activity in a collection", %{conn: conn} do
462 note_activity = insert(:note_activity)
463 note_object = Object.normalize(note_activity)
464 user = User.get_cached_by_ap_id(note_activity.data["actor"])
468 |> put_req_header("accept", "application/activity+json")
469 |> get("/users/#{user.nickname}/outbox")
471 assert response(conn, 200) =~ note_object.data["content"]
474 test "it returns an announce activity in a collection", %{conn: conn} do
475 announce_activity = insert(:announce_activity)
476 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
480 |> put_req_header("accept", "application/activity+json")
481 |> get("/users/#{user.nickname}/outbox")
483 assert response(conn, 200) =~ announce_activity.data["object"]
486 test "it rejects posts from other users", %{conn: conn} do
487 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
489 otheruser = insert(:user)
493 |> assign(:user, otheruser)
494 |> put_req_header("content-type", "application/activity+json")
495 |> post("/users/#{user.nickname}/outbox", data)
497 assert json_response(conn, 403)
500 test "it inserts an incoming create activity into the database", %{conn: conn} do
501 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
506 |> assign(:user, user)
507 |> put_req_header("content-type", "application/activity+json")
508 |> post("/users/#{user.nickname}/outbox", data)
510 result = json_response(conn, 201)
512 assert Activity.get_by_ap_id(result["id"])
515 test "it rejects an incoming activity with bogus type", %{conn: conn} do
516 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
521 |> Map.put("type", "BadType")
525 |> assign(:user, user)
526 |> put_req_header("content-type", "application/activity+json")
527 |> post("/users/#{user.nickname}/outbox", data)
529 assert json_response(conn, 400)
532 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
533 note_activity = insert(:note_activity)
534 note_object = Object.normalize(note_activity)
535 user = User.get_cached_by_ap_id(note_activity.data["actor"])
540 id: note_object.data["id"]
546 |> assign(:user, user)
547 |> put_req_header("content-type", "application/activity+json")
548 |> post("/users/#{user.nickname}/outbox", data)
550 result = json_response(conn, 201)
551 assert Activity.get_by_ap_id(result["id"])
553 assert object = Object.get_by_ap_id(note_object.data["id"])
554 assert object.data["type"] == "Tombstone"
557 test "it rejects delete activity of object from other actor", %{conn: conn} do
558 note_activity = insert(:note_activity)
559 note_object = Object.normalize(note_activity)
565 id: note_object.data["id"]
571 |> assign(:user, user)
572 |> put_req_header("content-type", "application/activity+json")
573 |> post("/users/#{user.nickname}/outbox", data)
575 assert json_response(conn, 400)
578 test "it increases like count when receiving a like action", %{conn: conn} do
579 note_activity = insert(:note_activity)
580 note_object = Object.normalize(note_activity)
581 user = User.get_cached_by_ap_id(note_activity.data["actor"])
586 id: note_object.data["id"]
592 |> assign(:user, user)
593 |> put_req_header("content-type", "application/activity+json")
594 |> post("/users/#{user.nickname}/outbox", data)
596 result = json_response(conn, 201)
597 assert Activity.get_by_ap_id(result["id"])
599 assert object = Object.get_by_ap_id(note_object.data["id"])
600 assert object.data["like_count"] == 1
604 describe "/users/:nickname/followers" do
605 test "it returns the followers in a collection", %{conn: conn} do
607 user_two = insert(:user)
608 User.follow(user, user_two)
612 |> get("/users/#{user_two.nickname}/followers")
613 |> json_response(200)
615 assert result["first"]["orderedItems"] == [user.ap_id]
618 test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
620 user_two = insert(:user, %{info: %{hide_followers: true}})
621 User.follow(user, user_two)
625 |> get("/users/#{user_two.nickname}/followers")
626 |> json_response(200)
628 assert is_binary(result["first"])
631 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
633 user = insert(:user, %{info: %{hide_followers: true}})
637 |> get("/users/#{user.nickname}/followers?page=1")
639 assert result.status == 403
640 assert result.resp_body == ""
643 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
645 user = insert(:user, %{info: %{hide_followers: true}})
646 other_user = insert(:user)
647 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
651 |> assign(:user, user)
652 |> get("/users/#{user.nickname}/followers?page=1")
653 |> json_response(200)
655 assert result["totalItems"] == 1
656 assert result["orderedItems"] == [other_user.ap_id]
659 test "it works for more than 10 users", %{conn: conn} do
662 Enum.each(1..15, fn _ ->
663 other_user = insert(:user)
664 User.follow(other_user, user)
669 |> get("/users/#{user.nickname}/followers")
670 |> json_response(200)
672 assert length(result["first"]["orderedItems"]) == 10
673 assert result["first"]["totalItems"] == 15
674 assert result["totalItems"] == 15
678 |> get("/users/#{user.nickname}/followers?page=2")
679 |> json_response(200)
681 assert length(result["orderedItems"]) == 5
682 assert result["totalItems"] == 15
686 describe "/users/:nickname/following" do
687 test "it returns the following in a collection", %{conn: conn} do
689 user_two = insert(:user)
690 User.follow(user, user_two)
694 |> get("/users/#{user.nickname}/following")
695 |> json_response(200)
697 assert result["first"]["orderedItems"] == [user_two.ap_id]
700 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
701 user = insert(:user, %{info: %{hide_follows: true}})
702 user_two = insert(:user)
703 User.follow(user, user_two)
707 |> get("/users/#{user.nickname}/following")
708 |> json_response(200)
710 assert is_binary(result["first"])
713 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
715 user = insert(:user, %{info: %{hide_follows: true}})
719 |> get("/users/#{user.nickname}/following?page=1")
721 assert result.status == 403
722 assert result.resp_body == ""
725 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
727 user = insert(:user, %{info: %{hide_follows: true}})
728 other_user = insert(:user)
729 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
733 |> assign(:user, user)
734 |> get("/users/#{user.nickname}/following?page=1")
735 |> json_response(200)
737 assert result["totalItems"] == 1
738 assert result["orderedItems"] == [other_user.ap_id]
741 test "it works for more than 10 users", %{conn: conn} do
744 Enum.each(1..15, fn _ ->
745 user = User.get_cached_by_id(user.id)
746 other_user = insert(:user)
747 User.follow(user, other_user)
752 |> get("/users/#{user.nickname}/following")
753 |> json_response(200)
755 assert length(result["first"]["orderedItems"]) == 10
756 assert result["first"]["totalItems"] == 15
757 assert result["totalItems"] == 15
761 |> get("/users/#{user.nickname}/following?page=2")
762 |> json_response(200)
764 assert length(result["orderedItems"]) == 5
765 assert result["totalItems"] == 15