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