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