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
9 alias Pleroma.Instances
12 alias Pleroma.Web.ActivityPub.ObjectView
13 alias Pleroma.Web.ActivityPub.UserView
14 alias Pleroma.Web.ActivityPub.Utils
15 alias Pleroma.Web.CommonAPI
18 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
20 config_path = [:instance, :federating]
21 initial_setting = Pleroma.Config.get(config_path)
23 Pleroma.Config.put(config_path, true)
24 on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
30 test "with the relay active, it returns the relay user", %{conn: conn} do
33 |> get(activity_pub_path(conn, :relay))
36 assert res["id"] =~ "/relay"
39 test "with the relay disabled, it returns 404", %{conn: conn} do
40 Pleroma.Config.put([:instance, :allow_relay], false)
43 |> get(activity_pub_path(conn, :relay))
47 Pleroma.Config.put([:instance, :allow_relay], true)
51 describe "/internal/fetch" do
52 test "it returns the internal fetch user", %{conn: conn} do
55 |> get(activity_pub_path(conn, :internal_fetch))
58 assert res["id"] =~ "/fetch"
62 describe "/users/:nickname" do
63 test "it returns a json representation of the user with accept application/json", %{
70 |> put_req_header("accept", "application/json")
71 |> get("/users/#{user.nickname}")
73 user = User.get_cached_by_id(user.id)
75 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
78 test "it returns a json representation of the user with accept application/activity+json", %{
85 |> put_req_header("accept", "application/activity+json")
86 |> get("/users/#{user.nickname}")
88 user = User.get_cached_by_id(user.id)
90 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
93 test "it returns a json representation of the user with accept application/ld+json", %{
102 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
104 |> get("/users/#{user.nickname}")
106 user = User.get_cached_by_id(user.id)
108 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
112 describe "/object/:uuid" do
113 test "it returns a json representation of the object with accept application/json", %{
117 uuid = String.split(note.data["id"], "/") |> List.last()
121 |> put_req_header("accept", "application/json")
122 |> get("/objects/#{uuid}")
124 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
127 test "it returns a json representation of the object with accept application/activity+json",
130 uuid = String.split(note.data["id"], "/") |> List.last()
134 |> put_req_header("accept", "application/activity+json")
135 |> get("/objects/#{uuid}")
137 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
140 test "it returns a json representation of the object with accept application/ld+json", %{
144 uuid = String.split(note.data["id"], "/") |> List.last()
150 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
152 |> get("/objects/#{uuid}")
154 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
157 test "it returns 404 for non-public messages", %{conn: conn} do
158 note = insert(:direct_note)
159 uuid = String.split(note.data["id"], "/") |> List.last()
163 |> put_req_header("accept", "application/activity+json")
164 |> get("/objects/#{uuid}")
166 assert json_response(conn, 404)
169 test "it returns 404 for tombstone objects", %{conn: conn} do
170 tombstone = insert(:tombstone)
171 uuid = String.split(tombstone.data["id"], "/") |> List.last()
175 |> put_req_header("accept", "application/activity+json")
176 |> get("/objects/#{uuid}")
178 assert json_response(conn, 404)
182 describe "/object/:uuid/likes" do
184 like = insert(:like_activity)
185 like_object_ap_id = Object.normalize(like).data["id"]
192 [id: like.data["id"], uuid: uuid]
195 test "it returns the like activities in a collection", %{conn: conn, id: id, uuid: uuid} do
198 |> put_req_header("accept", "application/activity+json")
199 |> get("/objects/#{uuid}/likes")
200 |> json_response(200)
202 assert List.first(result["first"]["orderedItems"])["id"] == id
203 assert result["type"] == "OrderedCollection"
204 assert result["totalItems"] == 1
205 refute result["first"]["next"]
208 test "it does not crash when page number is exceeded total pages", %{conn: conn, uuid: uuid} do
211 |> put_req_header("accept", "application/activity+json")
212 |> get("/objects/#{uuid}/likes?page=2")
213 |> json_response(200)
215 assert result["type"] == "OrderedCollectionPage"
216 assert result["totalItems"] == 1
217 refute result["next"]
218 assert Enum.empty?(result["orderedItems"])
221 test "it contains the next key when likes count is more than 10", %{conn: conn} do
222 note = insert(:note_activity)
223 insert_list(11, :like_activity, note_activity: note)
227 |> Object.normalize()
235 |> put_req_header("accept", "application/activity+json")
236 |> get("/objects/#{uuid}/likes?page=1")
237 |> json_response(200)
239 assert result["totalItems"] == 11
240 assert length(result["orderedItems"]) == 10
241 assert result["next"]
245 describe "/activities/:uuid" do
246 test "it returns a json representation of the activity", %{conn: conn} do
247 activity = insert(:note_activity)
248 uuid = String.split(activity.data["id"], "/") |> List.last()
252 |> put_req_header("accept", "application/activity+json")
253 |> get("/activities/#{uuid}")
255 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
258 test "it returns 404 for non-public activities", %{conn: conn} do
259 activity = insert(:direct_note_activity)
260 uuid = String.split(activity.data["id"], "/") |> List.last()
264 |> put_req_header("accept", "application/activity+json")
265 |> get("/activities/#{uuid}")
267 assert json_response(conn, 404)
272 test "it inserts an incoming activity into the database", %{conn: conn} do
273 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
277 |> assign(:valid_signature, true)
278 |> put_req_header("content-type", "application/activity+json")
279 |> post("/inbox", data)
281 assert "ok" == json_response(conn, 200)
283 assert Activity.get_by_ap_id(data["id"])
286 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
287 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
289 sender_url = data["actor"]
290 Instances.set_consistently_unreachable(sender_url)
291 refute Instances.reachable?(sender_url)
295 |> assign(:valid_signature, true)
296 |> put_req_header("content-type", "application/activity+json")
297 |> post("/inbox", data)
299 assert "ok" == json_response(conn, 200)
300 assert Instances.reachable?(sender_url)
304 describe "/users/:nickname/inbox" do
307 File.read!("test/fixtures/mastodon-post-activity.json")
313 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
315 data = Map.put(data, "bcc", [user.ap_id])
319 |> assign(:valid_signature, true)
320 |> put_req_header("content-type", "application/activity+json")
321 |> post("/users/#{user.nickname}/inbox", data)
323 assert "ok" == json_response(conn, 200)
325 assert Activity.get_by_ap_id(data["id"])
328 test "it accepts messages from actors that are followed by the user", %{
332 recipient = insert(:user)
333 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
335 {:ok, recipient} = User.follow(recipient, actor)
339 |> Map.put("attributedTo", actor.ap_id)
343 |> Map.put("actor", actor.ap_id)
344 |> Map.put("object", object)
348 |> assign(:valid_signature, true)
349 |> put_req_header("content-type", "application/activity+json")
350 |> post("/users/#{recipient.nickname}/inbox", data)
352 assert "ok" == json_response(conn, 200)
354 assert Activity.get_by_ap_id(data["id"])
357 test "it rejects reads from other users", %{conn: conn} do
359 otheruser = insert(:user)
363 |> assign(:user, otheruser)
364 |> put_req_header("accept", "application/activity+json")
365 |> get("/users/#{user.nickname}/inbox")
367 assert json_response(conn, 403)
370 test "it returns a note activity in a collection", %{conn: conn} do
371 note_activity = insert(:direct_note_activity)
372 note_object = Object.normalize(note_activity)
373 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
377 |> assign(:user, user)
378 |> put_req_header("accept", "application/activity+json")
379 |> get("/users/#{user.nickname}/inbox")
381 assert response(conn, 200) =~ note_object.data["content"]
384 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
386 data = Map.put(data, "bcc", [user.ap_id])
388 sender_host = URI.parse(data["actor"]).host
389 Instances.set_consistently_unreachable(sender_host)
390 refute Instances.reachable?(sender_host)
394 |> assign(:valid_signature, true)
395 |> put_req_header("content-type", "application/activity+json")
396 |> post("/users/#{user.nickname}/inbox", data)
398 assert "ok" == json_response(conn, 200)
399 assert Instances.reachable?(sender_host)
402 test "it removes all follower collections but actor's", %{conn: conn} do
403 [actor, recipient] = insert_pair(:user)
406 File.read!("test/fixtures/activitypub-client-post-activity.json")
409 object = Map.put(data["object"], "attributedTo", actor.ap_id)
413 |> Map.put("id", Utils.generate_object_id())
414 |> Map.put("actor", actor.ap_id)
415 |> Map.put("object", object)
417 recipient.follower_address,
418 actor.follower_address
422 recipient.follower_address,
423 "https://www.w3.org/ns/activitystreams#Public"
427 |> assign(:valid_signature, true)
428 |> put_req_header("content-type", "application/activity+json")
429 |> post("/users/#{recipient.nickname}/inbox", data)
430 |> json_response(200)
432 activity = Activity.get_by_ap_id(data["id"])
435 assert actor.follower_address in activity.recipients
436 assert actor.follower_address in activity.data["cc"]
438 refute recipient.follower_address in activity.recipients
439 refute recipient.follower_address in activity.data["cc"]
440 refute recipient.follower_address in activity.data["to"]
444 describe "/users/:nickname/outbox" do
445 test "it will not bomb when there is no activity", %{conn: conn} do
450 |> put_req_header("accept", "application/activity+json")
451 |> get("/users/#{user.nickname}/outbox")
453 result = json_response(conn, 200)
454 assert user.ap_id <> "/outbox" == result["id"]
457 test "it returns a note activity in a collection", %{conn: conn} do
458 note_activity = insert(:note_activity)
459 note_object = Object.normalize(note_activity)
460 user = User.get_cached_by_ap_id(note_activity.data["actor"])
464 |> put_req_header("accept", "application/activity+json")
465 |> get("/users/#{user.nickname}/outbox")
467 assert response(conn, 200) =~ note_object.data["content"]
470 test "it returns an announce activity in a collection", %{conn: conn} do
471 announce_activity = insert(:announce_activity)
472 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
476 |> put_req_header("accept", "application/activity+json")
477 |> get("/users/#{user.nickname}/outbox")
479 assert response(conn, 200) =~ announce_activity.data["object"]
482 test "it rejects posts from other users", %{conn: conn} do
483 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
485 otheruser = insert(:user)
489 |> assign(:user, otheruser)
490 |> put_req_header("content-type", "application/activity+json")
491 |> post("/users/#{user.nickname}/outbox", data)
493 assert json_response(conn, 403)
496 test "it inserts an incoming create activity into the database", %{conn: conn} do
497 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
502 |> assign(:user, user)
503 |> put_req_header("content-type", "application/activity+json")
504 |> post("/users/#{user.nickname}/outbox", data)
506 result = json_response(conn, 201)
507 assert Activity.get_by_ap_id(result["id"])
510 test "it rejects an incoming activity with bogus type", %{conn: conn} do
511 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
516 |> Map.put("type", "BadType")
520 |> assign(:user, user)
521 |> put_req_header("content-type", "application/activity+json")
522 |> post("/users/#{user.nickname}/outbox", data)
524 assert json_response(conn, 400)
527 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
528 note_activity = insert(:note_activity)
529 note_object = Object.normalize(note_activity)
530 user = User.get_cached_by_ap_id(note_activity.data["actor"])
535 id: note_object.data["id"]
541 |> assign(:user, user)
542 |> put_req_header("content-type", "application/activity+json")
543 |> post("/users/#{user.nickname}/outbox", data)
545 result = json_response(conn, 201)
546 assert Activity.get_by_ap_id(result["id"])
548 assert object = Object.get_by_ap_id(note_object.data["id"])
549 assert object.data["type"] == "Tombstone"
552 test "it rejects delete activity of object from other actor", %{conn: conn} do
553 note_activity = insert(:note_activity)
554 note_object = Object.normalize(note_activity)
560 id: note_object.data["id"]
566 |> assign(:user, user)
567 |> put_req_header("content-type", "application/activity+json")
568 |> post("/users/#{user.nickname}/outbox", data)
570 assert json_response(conn, 400)
573 test "it increases like count when receiving a like action", %{conn: conn} do
574 note_activity = insert(:note_activity)
575 note_object = Object.normalize(note_activity)
576 user = User.get_cached_by_ap_id(note_activity.data["actor"])
581 id: note_object.data["id"]
587 |> assign(:user, user)
588 |> put_req_header("content-type", "application/activity+json")
589 |> post("/users/#{user.nickname}/outbox", data)
591 result = json_response(conn, 201)
592 assert Activity.get_by_ap_id(result["id"])
594 assert object = Object.get_by_ap_id(note_object.data["id"])
595 assert object.data["like_count"] == 1
599 describe "/users/:nickname/followers" do
600 test "it returns the followers in a collection", %{conn: conn} do
602 user_two = insert(:user)
603 User.follow(user, user_two)
607 |> get("/users/#{user_two.nickname}/followers")
608 |> json_response(200)
610 assert result["first"]["orderedItems"] == [user.ap_id]
613 test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
615 user_two = insert(:user, %{info: %{hide_followers: true}})
616 User.follow(user, user_two)
620 |> get("/users/#{user_two.nickname}/followers")
621 |> json_response(200)
623 assert is_binary(result["first"])
626 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
628 user = insert(:user, %{info: %{hide_followers: true}})
632 |> get("/users/#{user.nickname}/followers?page=1")
634 assert result.status == 403
635 assert result.resp_body == ""
638 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
640 user = insert(:user, %{info: %{hide_followers: true}})
641 other_user = insert(:user)
642 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
646 |> assign(:user, user)
647 |> get("/users/#{user.nickname}/followers?page=1")
648 |> json_response(200)
650 assert result["totalItems"] == 1
651 assert result["orderedItems"] == [other_user.ap_id]
654 test "it works for more than 10 users", %{conn: conn} do
657 Enum.each(1..15, fn _ ->
658 other_user = insert(:user)
659 User.follow(other_user, user)
664 |> get("/users/#{user.nickname}/followers")
665 |> json_response(200)
667 assert length(result["first"]["orderedItems"]) == 10
668 assert result["first"]["totalItems"] == 15
669 assert result["totalItems"] == 15
673 |> get("/users/#{user.nickname}/followers?page=2")
674 |> json_response(200)
676 assert length(result["orderedItems"]) == 5
677 assert result["totalItems"] == 15
681 describe "/users/:nickname/following" do
682 test "it returns the following in a collection", %{conn: conn} do
684 user_two = insert(:user)
685 User.follow(user, user_two)
689 |> get("/users/#{user.nickname}/following")
690 |> json_response(200)
692 assert result["first"]["orderedItems"] == [user_two.ap_id]
695 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
696 user = insert(:user, %{info: %{hide_follows: true}})
697 user_two = insert(:user)
698 User.follow(user, user_two)
702 |> get("/users/#{user.nickname}/following")
703 |> json_response(200)
705 assert is_binary(result["first"])
708 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
710 user = insert(:user, %{info: %{hide_follows: true}})
714 |> get("/users/#{user.nickname}/following?page=1")
716 assert result.status == 403
717 assert result.resp_body == ""
720 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
722 user = insert(:user, %{info: %{hide_follows: true}})
723 other_user = insert(:user)
724 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
728 |> assign(:user, user)
729 |> get("/users/#{user.nickname}/following?page=1")
730 |> json_response(200)
732 assert result["totalItems"] == 1
733 assert result["orderedItems"] == [other_user.ap_id]
736 test "it works for more than 10 users", %{conn: conn} do
739 Enum.each(1..15, fn _ ->
740 user = User.get_cached_by_id(user.id)
741 other_user = insert(:user)
742 User.follow(user, other_user)
747 |> get("/users/#{user.nickname}/following")
748 |> json_response(200)
750 assert length(result["first"]["orderedItems"]) == 10
751 assert result["first"]["totalItems"] == 15
752 assert result["totalItems"] == 15
756 |> get("/users/#{user.nickname}/following?page=2")
757 |> json_response(200)
759 assert length(result["orderedItems"]) == 5
760 assert result["totalItems"] == 15