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)
22 clear_config_all([:instance, :federating],
23 do: Pleroma.Config.put([:instance, :federating], true)
27 clear_config([:instance, :allow_relay])
29 test "with the relay active, it returns the relay user", %{conn: conn} do
32 |> get(activity_pub_path(conn, :relay))
35 assert res["id"] =~ "/relay"
38 test "with the relay disabled, it returns 404", %{conn: conn} do
39 Pleroma.Config.put([:instance, :allow_relay], false)
42 |> get(activity_pub_path(conn, :relay))
48 describe "/internal/fetch" do
49 test "it returns the internal fetch user", %{conn: conn} do
52 |> get(activity_pub_path(conn, :internal_fetch))
55 assert res["id"] =~ "/fetch"
59 describe "/users/:nickname" do
60 test "it returns a json representation of the user with accept application/json", %{
67 |> put_req_header("accept", "application/json")
68 |> get("/users/#{user.nickname}")
70 user = User.get_cached_by_id(user.id)
72 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
75 test "it returns a json representation of the user with accept application/activity+json", %{
82 |> put_req_header("accept", "application/activity+json")
83 |> get("/users/#{user.nickname}")
85 user = User.get_cached_by_id(user.id)
87 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
90 test "it returns a json representation of the user with accept application/ld+json", %{
99 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
101 |> get("/users/#{user.nickname}")
103 user = User.get_cached_by_id(user.id)
105 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
109 describe "/object/:uuid" do
110 test "it returns a json representation of the object with accept application/json", %{
114 uuid = String.split(note.data["id"], "/") |> List.last()
118 |> put_req_header("accept", "application/json")
119 |> get("/objects/#{uuid}")
121 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
124 test "it returns a json representation of the object with accept application/activity+json",
127 uuid = String.split(note.data["id"], "/") |> List.last()
131 |> put_req_header("accept", "application/activity+json")
132 |> get("/objects/#{uuid}")
134 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
137 test "it returns a json representation of the object with accept application/ld+json", %{
141 uuid = String.split(note.data["id"], "/") |> List.last()
147 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
149 |> get("/objects/#{uuid}")
151 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
154 test "it returns 404 for non-public messages", %{conn: conn} do
155 note = insert(:direct_note)
156 uuid = String.split(note.data["id"], "/") |> List.last()
160 |> put_req_header("accept", "application/activity+json")
161 |> get("/objects/#{uuid}")
163 assert json_response(conn, 404)
166 test "it returns 404 for tombstone objects", %{conn: conn} do
167 tombstone = insert(:tombstone)
168 uuid = String.split(tombstone.data["id"], "/") |> List.last()
172 |> put_req_header("accept", "application/activity+json")
173 |> get("/objects/#{uuid}")
175 assert json_response(conn, 404)
179 describe "/object/:uuid/likes" do
181 like = insert(:like_activity)
182 like_object_ap_id = Object.normalize(like).data["id"]
189 [id: like.data["id"], uuid: uuid]
192 test "it returns the like activities in a collection", %{conn: conn, id: id, uuid: uuid} do
195 |> put_req_header("accept", "application/activity+json")
196 |> get("/objects/#{uuid}/likes")
197 |> json_response(200)
199 assert List.first(result["first"]["orderedItems"])["id"] == id
200 assert result["type"] == "OrderedCollection"
201 assert result["totalItems"] == 1
202 refute result["first"]["next"]
205 test "it does not crash when page number is exceeded total pages", %{conn: conn, uuid: uuid} do
208 |> put_req_header("accept", "application/activity+json")
209 |> get("/objects/#{uuid}/likes?page=2")
210 |> json_response(200)
212 assert result["type"] == "OrderedCollectionPage"
213 assert result["totalItems"] == 1
214 refute result["next"]
215 assert Enum.empty?(result["orderedItems"])
218 test "it contains the next key when likes count is more than 10", %{conn: conn} do
219 note = insert(:note_activity)
220 insert_list(11, :like_activity, note_activity: note)
224 |> Object.normalize()
232 |> put_req_header("accept", "application/activity+json")
233 |> get("/objects/#{uuid}/likes?page=1")
234 |> json_response(200)
236 assert result["totalItems"] == 11
237 assert length(result["orderedItems"]) == 10
238 assert result["next"]
242 describe "/activities/:uuid" do
243 test "it returns a json representation of the activity", %{conn: conn} do
244 activity = insert(:note_activity)
245 uuid = String.split(activity.data["id"], "/") |> List.last()
249 |> put_req_header("accept", "application/activity+json")
250 |> get("/activities/#{uuid}")
252 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
255 test "it returns 404 for non-public activities", %{conn: conn} do
256 activity = insert(:direct_note_activity)
257 uuid = String.split(activity.data["id"], "/") |> List.last()
261 |> put_req_header("accept", "application/activity+json")
262 |> get("/activities/#{uuid}")
264 assert json_response(conn, 404)
269 test "it inserts an incoming activity into the database", %{conn: conn} do
270 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
274 |> assign(:valid_signature, true)
275 |> put_req_header("content-type", "application/activity+json")
276 |> post("/inbox", data)
278 assert "ok" == json_response(conn, 200)
280 assert Activity.get_by_ap_id(data["id"])
283 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
284 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
286 sender_url = data["actor"]
287 Instances.set_consistently_unreachable(sender_url)
288 refute Instances.reachable?(sender_url)
292 |> assign(:valid_signature, true)
293 |> put_req_header("content-type", "application/activity+json")
294 |> post("/inbox", data)
296 assert "ok" == json_response(conn, 200)
297 assert Instances.reachable?(sender_url)
301 describe "/users/:nickname/inbox" do
304 File.read!("test/fixtures/mastodon-post-activity.json")
310 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
312 data = Map.put(data, "bcc", [user.ap_id])
316 |> assign(:valid_signature, true)
317 |> put_req_header("content-type", "application/activity+json")
318 |> post("/users/#{user.nickname}/inbox", data)
320 assert "ok" == json_response(conn, 200)
322 assert Activity.get_by_ap_id(data["id"])
325 test "it accepts messages from actors that are followed by the user", %{
329 recipient = insert(:user)
330 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
332 {:ok, recipient} = User.follow(recipient, actor)
336 |> Map.put("attributedTo", actor.ap_id)
340 |> Map.put("actor", actor.ap_id)
341 |> Map.put("object", object)
345 |> assign(:valid_signature, true)
346 |> put_req_header("content-type", "application/activity+json")
347 |> post("/users/#{recipient.nickname}/inbox", data)
349 assert "ok" == json_response(conn, 200)
351 assert Activity.get_by_ap_id(data["id"])
354 test "it rejects reads from other users", %{conn: conn} do
356 otheruser = insert(:user)
360 |> assign(:user, otheruser)
361 |> put_req_header("accept", "application/activity+json")
362 |> get("/users/#{user.nickname}/inbox")
364 assert json_response(conn, 403)
367 test "it returns a note activity in a collection", %{conn: conn} do
368 note_activity = insert(:direct_note_activity)
369 note_object = Object.normalize(note_activity)
370 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
374 |> assign(:user, user)
375 |> put_req_header("accept", "application/activity+json")
376 |> get("/users/#{user.nickname}/inbox")
378 assert response(conn, 200) =~ note_object.data["content"]
381 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
383 data = Map.put(data, "bcc", [user.ap_id])
385 sender_host = URI.parse(data["actor"]).host
386 Instances.set_consistently_unreachable(sender_host)
387 refute Instances.reachable?(sender_host)
391 |> assign(:valid_signature, true)
392 |> put_req_header("content-type", "application/activity+json")
393 |> post("/users/#{user.nickname}/inbox", data)
395 assert "ok" == json_response(conn, 200)
396 assert Instances.reachable?(sender_host)
399 test "it removes all follower collections but actor's", %{conn: conn} do
400 [actor, recipient] = insert_pair(:user)
403 File.read!("test/fixtures/activitypub-client-post-activity.json")
406 object = Map.put(data["object"], "attributedTo", actor.ap_id)
410 |> Map.put("id", Utils.generate_object_id())
411 |> Map.put("actor", actor.ap_id)
412 |> Map.put("object", object)
414 recipient.follower_address,
415 actor.follower_address
419 recipient.follower_address,
420 "https://www.w3.org/ns/activitystreams#Public"
424 |> assign(:valid_signature, true)
425 |> put_req_header("content-type", "application/activity+json")
426 |> post("/users/#{recipient.nickname}/inbox", data)
427 |> json_response(200)
429 activity = Activity.get_by_ap_id(data["id"])
432 assert actor.follower_address in activity.recipients
433 assert actor.follower_address in activity.data["cc"]
435 refute recipient.follower_address in activity.recipients
436 refute recipient.follower_address in activity.data["cc"]
437 refute recipient.follower_address in activity.data["to"]
441 describe "/users/:nickname/outbox" do
442 test "it will not bomb when there is no activity", %{conn: conn} do
447 |> put_req_header("accept", "application/activity+json")
448 |> get("/users/#{user.nickname}/outbox")
450 result = json_response(conn, 200)
451 assert user.ap_id <> "/outbox" == result["id"]
454 test "it returns a note activity in a collection", %{conn: conn} do
455 note_activity = insert(:note_activity)
456 note_object = Object.normalize(note_activity)
457 user = User.get_cached_by_ap_id(note_activity.data["actor"])
461 |> put_req_header("accept", "application/activity+json")
462 |> get("/users/#{user.nickname}/outbox")
464 assert response(conn, 200) =~ note_object.data["content"]
467 test "it returns an announce activity in a collection", %{conn: conn} do
468 announce_activity = insert(:announce_activity)
469 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
473 |> put_req_header("accept", "application/activity+json")
474 |> get("/users/#{user.nickname}/outbox")
476 assert response(conn, 200) =~ announce_activity.data["object"]
479 test "it rejects posts from other users", %{conn: conn} do
480 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
482 otheruser = insert(:user)
486 |> assign(:user, otheruser)
487 |> put_req_header("content-type", "application/activity+json")
488 |> post("/users/#{user.nickname}/outbox", data)
490 assert json_response(conn, 403)
493 test "it inserts an incoming create activity into the database", %{conn: conn} do
494 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
499 |> assign(:user, user)
500 |> put_req_header("content-type", "application/activity+json")
501 |> post("/users/#{user.nickname}/outbox", data)
503 result = json_response(conn, 201)
504 assert Activity.get_by_ap_id(result["id"])
507 test "it rejects an incoming activity with bogus type", %{conn: conn} do
508 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
513 |> Map.put("type", "BadType")
517 |> assign(:user, user)
518 |> put_req_header("content-type", "application/activity+json")
519 |> post("/users/#{user.nickname}/outbox", data)
521 assert json_response(conn, 400)
524 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
525 note_activity = insert(:note_activity)
526 note_object = Object.normalize(note_activity)
527 user = User.get_cached_by_ap_id(note_activity.data["actor"])
532 id: note_object.data["id"]
538 |> assign(:user, user)
539 |> put_req_header("content-type", "application/activity+json")
540 |> post("/users/#{user.nickname}/outbox", data)
542 result = json_response(conn, 201)
543 assert Activity.get_by_ap_id(result["id"])
545 assert object = Object.get_by_ap_id(note_object.data["id"])
546 assert object.data["type"] == "Tombstone"
549 test "it rejects delete activity of object from other actor", %{conn: conn} do
550 note_activity = insert(:note_activity)
551 note_object = Object.normalize(note_activity)
557 id: note_object.data["id"]
563 |> assign(:user, user)
564 |> put_req_header("content-type", "application/activity+json")
565 |> post("/users/#{user.nickname}/outbox", data)
567 assert json_response(conn, 400)
570 test "it increases like count when receiving a like action", %{conn: conn} do
571 note_activity = insert(:note_activity)
572 note_object = Object.normalize(note_activity)
573 user = User.get_cached_by_ap_id(note_activity.data["actor"])
578 id: note_object.data["id"]
584 |> assign(:user, user)
585 |> put_req_header("content-type", "application/activity+json")
586 |> post("/users/#{user.nickname}/outbox", data)
588 result = json_response(conn, 201)
589 assert Activity.get_by_ap_id(result["id"])
591 assert object = Object.get_by_ap_id(note_object.data["id"])
592 assert object.data["like_count"] == 1
596 describe "/users/:nickname/followers" do
597 test "it returns the followers in a collection", %{conn: conn} do
599 user_two = insert(:user)
600 User.follow(user, user_two)
604 |> get("/users/#{user_two.nickname}/followers")
605 |> json_response(200)
607 assert result["first"]["orderedItems"] == [user.ap_id]
610 test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
612 user_two = insert(:user, %{info: %{hide_followers: true}})
613 User.follow(user, user_two)
617 |> get("/users/#{user_two.nickname}/followers")
618 |> json_response(200)
620 assert is_binary(result["first"])
623 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
625 user = insert(:user, %{info: %{hide_followers: true}})
629 |> get("/users/#{user.nickname}/followers?page=1")
631 assert result.status == 403
632 assert result.resp_body == ""
635 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
637 user = insert(:user, %{info: %{hide_followers: true}})
638 other_user = insert(:user)
639 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
643 |> assign(:user, user)
644 |> get("/users/#{user.nickname}/followers?page=1")
645 |> json_response(200)
647 assert result["totalItems"] == 1
648 assert result["orderedItems"] == [other_user.ap_id]
651 test "it works for more than 10 users", %{conn: conn} do
654 Enum.each(1..15, fn _ ->
655 other_user = insert(:user)
656 User.follow(other_user, user)
661 |> get("/users/#{user.nickname}/followers")
662 |> json_response(200)
664 assert length(result["first"]["orderedItems"]) == 10
665 assert result["first"]["totalItems"] == 15
666 assert result["totalItems"] == 15
670 |> get("/users/#{user.nickname}/followers?page=2")
671 |> json_response(200)
673 assert length(result["orderedItems"]) == 5
674 assert result["totalItems"] == 15
678 describe "/users/:nickname/following" do
679 test "it returns the following in a collection", %{conn: conn} do
681 user_two = insert(:user)
682 User.follow(user, user_two)
686 |> get("/users/#{user.nickname}/following")
687 |> json_response(200)
689 assert result["first"]["orderedItems"] == [user_two.ap_id]
692 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
693 user = insert(:user, %{info: %{hide_follows: true}})
694 user_two = insert(:user)
695 User.follow(user, user_two)
699 |> get("/users/#{user.nickname}/following")
700 |> json_response(200)
702 assert is_binary(result["first"])
705 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
707 user = insert(:user, %{info: %{hide_follows: true}})
711 |> get("/users/#{user.nickname}/following?page=1")
713 assert result.status == 403
714 assert result.resp_body == ""
717 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
719 user = insert(:user, %{info: %{hide_follows: true}})
720 other_user = insert(:user)
721 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
725 |> assign(:user, user)
726 |> get("/users/#{user.nickname}/following?page=1")
727 |> json_response(200)
729 assert result["totalItems"] == 1
730 assert result["orderedItems"] == [other_user.ap_id]
733 test "it works for more than 10 users", %{conn: conn} do
736 Enum.each(1..15, fn _ ->
737 user = User.get_cached_by_id(user.id)
738 other_user = insert(:user)
739 User.follow(user, other_user)
744 |> get("/users/#{user.nickname}/following")
745 |> json_response(200)
747 assert length(result["first"]["orderedItems"]) == 10
748 assert result["first"]["totalItems"] == 15
749 assert result["totalItems"] == 15
753 |> get("/users/#{user.nickname}/following?page=2")
754 |> json_response(200)
756 assert length(result["orderedItems"]) == 5
757 assert result["totalItems"] == 15