77f5e39faab586fea0496258827b15fbb0c6c456
[akkoma] / test / web / activity_pub / activity_pub_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
6 use Pleroma.Web.ConnCase
7 import Pleroma.Factory
8 alias Pleroma.Activity
9 alias Pleroma.Instances
10 alias Pleroma.Object
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ObjectView
13 alias Pleroma.Web.ActivityPub.UserView
14 alias Pleroma.Web.ActivityPub.Utils
15 alias Pleroma.Web.CommonAPI
16
17 setup_all do
18 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
19 :ok
20 end
21
22 clear_config_all([:instance, :federating],
23 do: Pleroma.Config.put([:instance, :federating], true)
24 )
25
26 describe "/relay" do
27 clear_config([:instance, :allow_relay])
28
29 test "with the relay active, it returns the relay user", %{conn: conn} do
30 res =
31 conn
32 |> get(activity_pub_path(conn, :relay))
33 |> json_response(200)
34
35 assert res["id"] =~ "/relay"
36 end
37
38 test "with the relay disabled, it returns 404", %{conn: conn} do
39 Pleroma.Config.put([:instance, :allow_relay], false)
40
41 conn
42 |> get(activity_pub_path(conn, :relay))
43 |> json_response(404)
44 |> assert
45 end
46 end
47
48 describe "/internal/fetch" do
49 test "it returns the internal fetch user", %{conn: conn} do
50 res =
51 conn
52 |> get(activity_pub_path(conn, :internal_fetch))
53 |> json_response(200)
54
55 assert res["id"] =~ "/fetch"
56 end
57 end
58
59 describe "/users/:nickname" do
60 test "it returns a json representation of the user with accept application/json", %{
61 conn: conn
62 } do
63 user = insert(:user)
64
65 conn =
66 conn
67 |> put_req_header("accept", "application/json")
68 |> get("/users/#{user.nickname}")
69
70 user = User.get_cached_by_id(user.id)
71
72 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
73 end
74
75 test "it returns a json representation of the user with accept application/activity+json", %{
76 conn: conn
77 } do
78 user = insert(:user)
79
80 conn =
81 conn
82 |> put_req_header("accept", "application/activity+json")
83 |> get("/users/#{user.nickname}")
84
85 user = User.get_cached_by_id(user.id)
86
87 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
88 end
89
90 test "it returns a json representation of the user with accept application/ld+json", %{
91 conn: conn
92 } do
93 user = insert(:user)
94
95 conn =
96 conn
97 |> put_req_header(
98 "accept",
99 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
100 )
101 |> get("/users/#{user.nickname}")
102
103 user = User.get_cached_by_id(user.id)
104
105 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
106 end
107 end
108
109 describe "/object/:uuid" do
110 test "it returns a json representation of the object with accept application/json", %{
111 conn: conn
112 } do
113 note = insert(:note)
114 uuid = String.split(note.data["id"], "/") |> List.last()
115
116 conn =
117 conn
118 |> put_req_header("accept", "application/json")
119 |> get("/objects/#{uuid}")
120
121 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
122 end
123
124 test "it returns a json representation of the object with accept application/activity+json",
125 %{conn: conn} do
126 note = insert(:note)
127 uuid = String.split(note.data["id"], "/") |> List.last()
128
129 conn =
130 conn
131 |> put_req_header("accept", "application/activity+json")
132 |> get("/objects/#{uuid}")
133
134 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
135 end
136
137 test "it returns a json representation of the object with accept application/ld+json", %{
138 conn: conn
139 } do
140 note = insert(:note)
141 uuid = String.split(note.data["id"], "/") |> List.last()
142
143 conn =
144 conn
145 |> put_req_header(
146 "accept",
147 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
148 )
149 |> get("/objects/#{uuid}")
150
151 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
152 end
153
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()
157
158 conn =
159 conn
160 |> put_req_header("accept", "application/activity+json")
161 |> get("/objects/#{uuid}")
162
163 assert json_response(conn, 404)
164 end
165
166 test "it returns 404 for tombstone objects", %{conn: conn} do
167 tombstone = insert(:tombstone)
168 uuid = String.split(tombstone.data["id"], "/") |> List.last()
169
170 conn =
171 conn
172 |> put_req_header("accept", "application/activity+json")
173 |> get("/objects/#{uuid}")
174
175 assert json_response(conn, 404)
176 end
177 end
178
179 describe "/object/:uuid/likes" do
180 setup do
181 like = insert(:like_activity)
182 like_object_ap_id = Object.normalize(like).data["id"]
183
184 uuid =
185 like_object_ap_id
186 |> String.split("/")
187 |> List.last()
188
189 [id: like.data["id"], uuid: uuid]
190 end
191
192 test "it returns the like activities in a collection", %{conn: conn, id: id, uuid: uuid} do
193 result =
194 conn
195 |> put_req_header("accept", "application/activity+json")
196 |> get("/objects/#{uuid}/likes")
197 |> json_response(200)
198
199 assert List.first(result["first"]["orderedItems"])["id"] == id
200 assert result["type"] == "OrderedCollection"
201 assert result["totalItems"] == 1
202 refute result["first"]["next"]
203 end
204
205 test "it does not crash when page number is exceeded total pages", %{conn: conn, uuid: uuid} do
206 result =
207 conn
208 |> put_req_header("accept", "application/activity+json")
209 |> get("/objects/#{uuid}/likes?page=2")
210 |> json_response(200)
211
212 assert result["type"] == "OrderedCollectionPage"
213 assert result["totalItems"] == 1
214 refute result["next"]
215 assert Enum.empty?(result["orderedItems"])
216 end
217
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)
221
222 uuid =
223 note
224 |> Object.normalize()
225 |> Map.get(:data)
226 |> Map.get("id")
227 |> String.split("/")
228 |> List.last()
229
230 result =
231 conn
232 |> put_req_header("accept", "application/activity+json")
233 |> get("/objects/#{uuid}/likes?page=1")
234 |> json_response(200)
235
236 assert result["totalItems"] == 11
237 assert length(result["orderedItems"]) == 10
238 assert result["next"]
239 end
240 end
241
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()
246
247 conn =
248 conn
249 |> put_req_header("accept", "application/activity+json")
250 |> get("/activities/#{uuid}")
251
252 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
253 end
254
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()
258
259 conn =
260 conn
261 |> put_req_header("accept", "application/activity+json")
262 |> get("/activities/#{uuid}")
263
264 assert json_response(conn, 404)
265 end
266 end
267
268 describe "/inbox" do
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!()
271
272 conn =
273 conn
274 |> assign(:valid_signature, true)
275 |> put_req_header("content-type", "application/activity+json")
276 |> post("/inbox", data)
277
278 assert "ok" == json_response(conn, 200)
279 :timer.sleep(500)
280 assert Activity.get_by_ap_id(data["id"])
281 end
282
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!()
285
286 sender_url = data["actor"]
287 Instances.set_consistently_unreachable(sender_url)
288 refute Instances.reachable?(sender_url)
289
290 conn =
291 conn
292 |> assign(:valid_signature, true)
293 |> put_req_header("content-type", "application/activity+json")
294 |> post("/inbox", data)
295
296 assert "ok" == json_response(conn, 200)
297 assert Instances.reachable?(sender_url)
298 end
299 end
300
301 describe "/users/:nickname/inbox" do
302 setup do
303 data =
304 File.read!("test/fixtures/mastodon-post-activity.json")
305 |> Poison.decode!()
306
307 [data: data]
308 end
309
310 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
311 user = insert(:user)
312 data = Map.put(data, "bcc", [user.ap_id])
313
314 conn =
315 conn
316 |> assign(:valid_signature, true)
317 |> put_req_header("content-type", "application/activity+json")
318 |> post("/users/#{user.nickname}/inbox", data)
319
320 assert "ok" == json_response(conn, 200)
321 :timer.sleep(500)
322 assert Activity.get_by_ap_id(data["id"])
323 end
324
325 test "it accepts messages from actors that are followed by the user", %{
326 conn: conn,
327 data: data
328 } do
329 recipient = insert(:user)
330 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
331
332 {:ok, recipient} = User.follow(recipient, actor)
333
334 object =
335 data["object"]
336 |> Map.put("attributedTo", actor.ap_id)
337
338 data =
339 data
340 |> Map.put("actor", actor.ap_id)
341 |> Map.put("object", object)
342
343 conn =
344 conn
345 |> assign(:valid_signature, true)
346 |> put_req_header("content-type", "application/activity+json")
347 |> post("/users/#{recipient.nickname}/inbox", data)
348
349 assert "ok" == json_response(conn, 200)
350 :timer.sleep(500)
351 assert Activity.get_by_ap_id(data["id"])
352 end
353
354 test "it rejects reads from other users", %{conn: conn} do
355 user = insert(:user)
356 otheruser = insert(:user)
357
358 conn =
359 conn
360 |> assign(:user, otheruser)
361 |> put_req_header("accept", "application/activity+json")
362 |> get("/users/#{user.nickname}/inbox")
363
364 assert json_response(conn, 403)
365 end
366
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"]))
371
372 conn =
373 conn
374 |> assign(:user, user)
375 |> put_req_header("accept", "application/activity+json")
376 |> get("/users/#{user.nickname}/inbox")
377
378 assert response(conn, 200) =~ note_object.data["content"]
379 end
380
381 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
382 user = insert(:user)
383 data = Map.put(data, "bcc", [user.ap_id])
384
385 sender_host = URI.parse(data["actor"]).host
386 Instances.set_consistently_unreachable(sender_host)
387 refute Instances.reachable?(sender_host)
388
389 conn =
390 conn
391 |> assign(:valid_signature, true)
392 |> put_req_header("content-type", "application/activity+json")
393 |> post("/users/#{user.nickname}/inbox", data)
394
395 assert "ok" == json_response(conn, 200)
396 assert Instances.reachable?(sender_host)
397 end
398
399 test "it removes all follower collections but actor's", %{conn: conn} do
400 [actor, recipient] = insert_pair(:user)
401
402 data =
403 File.read!("test/fixtures/activitypub-client-post-activity.json")
404 |> Poison.decode!()
405
406 object = Map.put(data["object"], "attributedTo", actor.ap_id)
407
408 data =
409 data
410 |> Map.put("id", Utils.generate_object_id())
411 |> Map.put("actor", actor.ap_id)
412 |> Map.put("object", object)
413 |> Map.put("cc", [
414 recipient.follower_address,
415 actor.follower_address
416 ])
417 |> Map.put("to", [
418 recipient.ap_id,
419 recipient.follower_address,
420 "https://www.w3.org/ns/activitystreams#Public"
421 ])
422
423 conn
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)
428
429 activity = Activity.get_by_ap_id(data["id"])
430
431 assert activity.id
432 assert actor.follower_address in activity.recipients
433 assert actor.follower_address in activity.data["cc"]
434
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"]
438 end
439 end
440
441 describe "/users/:nickname/outbox" do
442 test "it will not bomb when there is no activity", %{conn: conn} do
443 user = insert(:user)
444
445 conn =
446 conn
447 |> put_req_header("accept", "application/activity+json")
448 |> get("/users/#{user.nickname}/outbox")
449
450 result = json_response(conn, 200)
451 assert user.ap_id <> "/outbox" == result["id"]
452 end
453
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"])
458
459 conn =
460 conn
461 |> put_req_header("accept", "application/activity+json")
462 |> get("/users/#{user.nickname}/outbox")
463
464 assert response(conn, 200) =~ note_object.data["content"]
465 end
466
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"])
470
471 conn =
472 conn
473 |> put_req_header("accept", "application/activity+json")
474 |> get("/users/#{user.nickname}/outbox")
475
476 assert response(conn, 200) =~ announce_activity.data["object"]
477 end
478
479 test "it rejects posts from other users", %{conn: conn} do
480 data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
481 user = insert(:user)
482 otheruser = insert(:user)
483
484 conn =
485 conn
486 |> assign(:user, otheruser)
487 |> put_req_header("content-type", "application/activity+json")
488 |> post("/users/#{user.nickname}/outbox", data)
489
490 assert json_response(conn, 403)
491 end
492
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!()
495 user = insert(:user)
496
497 conn =
498 conn
499 |> assign(:user, user)
500 |> put_req_header("content-type", "application/activity+json")
501 |> post("/users/#{user.nickname}/outbox", data)
502
503 result = json_response(conn, 201)
504 assert Activity.get_by_ap_id(result["id"])
505 end
506
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!()
509 user = insert(:user)
510
511 data =
512 data
513 |> Map.put("type", "BadType")
514
515 conn =
516 conn
517 |> assign(:user, user)
518 |> put_req_header("content-type", "application/activity+json")
519 |> post("/users/#{user.nickname}/outbox", data)
520
521 assert json_response(conn, 400)
522 end
523
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"])
528
529 data = %{
530 type: "Delete",
531 object: %{
532 id: note_object.data["id"]
533 }
534 }
535
536 conn =
537 conn
538 |> assign(:user, user)
539 |> put_req_header("content-type", "application/activity+json")
540 |> post("/users/#{user.nickname}/outbox", data)
541
542 result = json_response(conn, 201)
543 assert Activity.get_by_ap_id(result["id"])
544
545 assert object = Object.get_by_ap_id(note_object.data["id"])
546 assert object.data["type"] == "Tombstone"
547 end
548
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)
552 user = insert(:user)
553
554 data = %{
555 type: "Delete",
556 object: %{
557 id: note_object.data["id"]
558 }
559 }
560
561 conn =
562 conn
563 |> assign(:user, user)
564 |> put_req_header("content-type", "application/activity+json")
565 |> post("/users/#{user.nickname}/outbox", data)
566
567 assert json_response(conn, 400)
568 end
569
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"])
574
575 data = %{
576 type: "Like",
577 object: %{
578 id: note_object.data["id"]
579 }
580 }
581
582 conn =
583 conn
584 |> assign(:user, user)
585 |> put_req_header("content-type", "application/activity+json")
586 |> post("/users/#{user.nickname}/outbox", data)
587
588 result = json_response(conn, 201)
589 assert Activity.get_by_ap_id(result["id"])
590
591 assert object = Object.get_by_ap_id(note_object.data["id"])
592 assert object.data["like_count"] == 1
593 end
594 end
595
596 describe "/users/:nickname/followers" do
597 test "it returns the followers in a collection", %{conn: conn} do
598 user = insert(:user)
599 user_two = insert(:user)
600 User.follow(user, user_two)
601
602 result =
603 conn
604 |> get("/users/#{user_two.nickname}/followers")
605 |> json_response(200)
606
607 assert result["first"]["orderedItems"] == [user.ap_id]
608 end
609
610 test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
611 user = insert(:user)
612 user_two = insert(:user, %{info: %{hide_followers: true}})
613 User.follow(user, user_two)
614
615 result =
616 conn
617 |> get("/users/#{user_two.nickname}/followers")
618 |> json_response(200)
619
620 assert is_binary(result["first"])
621 end
622
623 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
624 %{conn: conn} do
625 user = insert(:user, %{info: %{hide_followers: true}})
626
627 result =
628 conn
629 |> get("/users/#{user.nickname}/followers?page=1")
630
631 assert result.status == 403
632 assert result.resp_body == ""
633 end
634
635 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
636 %{conn: conn} do
637 user = insert(:user, %{info: %{hide_followers: true}})
638 other_user = insert(:user)
639 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
640
641 result =
642 conn
643 |> assign(:user, user)
644 |> get("/users/#{user.nickname}/followers?page=1")
645 |> json_response(200)
646
647 assert result["totalItems"] == 1
648 assert result["orderedItems"] == [other_user.ap_id]
649 end
650
651 test "it works for more than 10 users", %{conn: conn} do
652 user = insert(:user)
653
654 Enum.each(1..15, fn _ ->
655 other_user = insert(:user)
656 User.follow(other_user, user)
657 end)
658
659 result =
660 conn
661 |> get("/users/#{user.nickname}/followers")
662 |> json_response(200)
663
664 assert length(result["first"]["orderedItems"]) == 10
665 assert result["first"]["totalItems"] == 15
666 assert result["totalItems"] == 15
667
668 result =
669 conn
670 |> get("/users/#{user.nickname}/followers?page=2")
671 |> json_response(200)
672
673 assert length(result["orderedItems"]) == 5
674 assert result["totalItems"] == 15
675 end
676 end
677
678 describe "/users/:nickname/following" do
679 test "it returns the following in a collection", %{conn: conn} do
680 user = insert(:user)
681 user_two = insert(:user)
682 User.follow(user, user_two)
683
684 result =
685 conn
686 |> get("/users/#{user.nickname}/following")
687 |> json_response(200)
688
689 assert result["first"]["orderedItems"] == [user_two.ap_id]
690 end
691
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)
696
697 result =
698 conn
699 |> get("/users/#{user.nickname}/following")
700 |> json_response(200)
701
702 assert is_binary(result["first"])
703 end
704
705 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
706 %{conn: conn} do
707 user = insert(:user, %{info: %{hide_follows: true}})
708
709 result =
710 conn
711 |> get("/users/#{user.nickname}/following?page=1")
712
713 assert result.status == 403
714 assert result.resp_body == ""
715 end
716
717 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
718 %{conn: conn} do
719 user = insert(:user, %{info: %{hide_follows: true}})
720 other_user = insert(:user)
721 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
722
723 result =
724 conn
725 |> assign(:user, user)
726 |> get("/users/#{user.nickname}/following?page=1")
727 |> json_response(200)
728
729 assert result["totalItems"] == 1
730 assert result["orderedItems"] == [other_user.ap_id]
731 end
732
733 test "it works for more than 10 users", %{conn: conn} do
734 user = insert(:user)
735
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)
740 end)
741
742 result =
743 conn
744 |> get("/users/#{user.nickname}/following")
745 |> json_response(200)
746
747 assert length(result["first"]["orderedItems"]) == 10
748 assert result["first"]["totalItems"] == 15
749 assert result["totalItems"] == 15
750
751 result =
752 conn
753 |> get("/users/#{user.nickname}/following?page=2")
754 |> json_response(200)
755
756 assert length(result["orderedItems"]) == 5
757 assert result["totalItems"] == 15
758 end
759 end
760 end