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