Merge branch 'develop' into feature/gen-magic
[akkoma] / test / web / activity_pub / activity_pub_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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 alias Pleroma.Activity
10 alias Pleroma.Config
11 alias Pleroma.Delivery
12 alias Pleroma.Instances
13 alias Pleroma.Object
14 alias Pleroma.Tests.ObanHelpers
15 alias Pleroma.User
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.ObjectView
18 alias Pleroma.Web.ActivityPub.Relay
19 alias Pleroma.Web.ActivityPub.UserView
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Workers.ReceiverWorker
24
25 import Pleroma.Factory
26
27 require Pleroma.Constants
28
29 setup_all do
30 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
31 :ok
32 end
33
34 setup do: clear_config([:instance, :federating], true)
35
36 describe "/relay" do
37 setup do: clear_config([:instance, :allow_relay])
38
39 test "with the relay active, it returns the relay user", %{conn: conn} do
40 res =
41 conn
42 |> get(activity_pub_path(conn, :relay))
43 |> json_response(200)
44
45 assert res["id"] =~ "/relay"
46 end
47
48 test "with the relay disabled, it returns 404", %{conn: conn} do
49 Config.put([:instance, :allow_relay], false)
50
51 conn
52 |> get(activity_pub_path(conn, :relay))
53 |> json_response(404)
54 end
55
56 test "on non-federating instance, it returns 404", %{conn: conn} do
57 Config.put([:instance, :federating], false)
58 user = insert(:user)
59
60 conn
61 |> assign(:user, user)
62 |> get(activity_pub_path(conn, :relay))
63 |> json_response(404)
64 end
65 end
66
67 describe "/internal/fetch" do
68 test "it returns the internal fetch user", %{conn: conn} do
69 res =
70 conn
71 |> get(activity_pub_path(conn, :internal_fetch))
72 |> json_response(200)
73
74 assert res["id"] =~ "/fetch"
75 end
76
77 test "on non-federating instance, it returns 404", %{conn: conn} do
78 Config.put([:instance, :federating], false)
79 user = insert(:user)
80
81 conn
82 |> assign(:user, user)
83 |> get(activity_pub_path(conn, :internal_fetch))
84 |> json_response(404)
85 end
86 end
87
88 describe "/users/:nickname" do
89 test "it returns a json representation of the user with accept application/json", %{
90 conn: conn
91 } do
92 user = insert(:user)
93
94 conn =
95 conn
96 |> put_req_header("accept", "application/json")
97 |> get("/users/#{user.nickname}")
98
99 user = User.get_cached_by_id(user.id)
100
101 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
102 end
103
104 test "it returns a json representation of the user with accept application/activity+json", %{
105 conn: conn
106 } do
107 user = insert(:user)
108
109 conn =
110 conn
111 |> put_req_header("accept", "application/activity+json")
112 |> get("/users/#{user.nickname}")
113
114 user = User.get_cached_by_id(user.id)
115
116 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
117 end
118
119 test "it returns a json representation of the user with accept application/ld+json", %{
120 conn: conn
121 } do
122 user = insert(:user)
123
124 conn =
125 conn
126 |> put_req_header(
127 "accept",
128 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
129 )
130 |> get("/users/#{user.nickname}")
131
132 user = User.get_cached_by_id(user.id)
133
134 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
135 end
136
137 test "it returns 404 for remote users", %{
138 conn: conn
139 } do
140 user = insert(:user, local: false, nickname: "remoteuser@example.com")
141
142 conn =
143 conn
144 |> put_req_header("accept", "application/json")
145 |> get("/users/#{user.nickname}.json")
146
147 assert json_response(conn, 404)
148 end
149
150 test "it returns error when user is not found", %{conn: conn} do
151 response =
152 conn
153 |> put_req_header("accept", "application/json")
154 |> get("/users/jimm")
155 |> json_response(404)
156
157 assert response == "Not found"
158 end
159
160 test "it requires authentication if instance is NOT federating", %{
161 conn: conn
162 } do
163 user = insert(:user)
164
165 conn =
166 put_req_header(
167 conn,
168 "accept",
169 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
170 )
171
172 ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user)
173 end
174 end
175
176 describe "mastodon compatibility routes" do
177 test "it returns a json representation of the object with accept application/json", %{
178 conn: conn
179 } do
180 {:ok, object} =
181 %{
182 "type" => "Note",
183 "content" => "hey",
184 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
185 "actor" => Endpoint.url() <> "/users/raymoo",
186 "to" => [Pleroma.Constants.as_public()]
187 }
188 |> Object.create()
189
190 conn =
191 conn
192 |> put_req_header("accept", "application/json")
193 |> get("/users/raymoo/statuses/999999999")
194
195 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
196 end
197
198 test "it returns a json representation of the activity with accept application/json", %{
199 conn: conn
200 } do
201 {:ok, object} =
202 %{
203 "type" => "Note",
204 "content" => "hey",
205 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
206 "actor" => Endpoint.url() <> "/users/raymoo",
207 "to" => [Pleroma.Constants.as_public()]
208 }
209 |> Object.create()
210
211 {:ok, activity, _} =
212 %{
213 "id" => object.data["id"] <> "/activity",
214 "type" => "Create",
215 "object" => object.data["id"],
216 "actor" => object.data["actor"],
217 "to" => object.data["to"]
218 }
219 |> ActivityPub.persist(local: true)
220
221 conn =
222 conn
223 |> put_req_header("accept", "application/json")
224 |> get("/users/raymoo/statuses/999999999/activity")
225
226 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
227 end
228 end
229
230 describe "/objects/:uuid" do
231 test "it returns a json representation of the object with accept application/json", %{
232 conn: conn
233 } do
234 note = insert(:note)
235 uuid = String.split(note.data["id"], "/") |> List.last()
236
237 conn =
238 conn
239 |> put_req_header("accept", "application/json")
240 |> get("/objects/#{uuid}")
241
242 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
243 end
244
245 test "it returns a json representation of the object with accept application/activity+json",
246 %{conn: conn} do
247 note = insert(:note)
248 uuid = String.split(note.data["id"], "/") |> List.last()
249
250 conn =
251 conn
252 |> put_req_header("accept", "application/activity+json")
253 |> get("/objects/#{uuid}")
254
255 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
256 end
257
258 test "it returns a json representation of the object with accept application/ld+json", %{
259 conn: conn
260 } do
261 note = insert(:note)
262 uuid = String.split(note.data["id"], "/") |> List.last()
263
264 conn =
265 conn
266 |> put_req_header(
267 "accept",
268 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
269 )
270 |> get("/objects/#{uuid}")
271
272 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
273 end
274
275 test "it returns 404 for non-public messages", %{conn: conn} do
276 note = insert(:direct_note)
277 uuid = String.split(note.data["id"], "/") |> List.last()
278
279 conn =
280 conn
281 |> put_req_header("accept", "application/activity+json")
282 |> get("/objects/#{uuid}")
283
284 assert json_response(conn, 404)
285 end
286
287 test "it returns 404 for tombstone objects", %{conn: conn} do
288 tombstone = insert(:tombstone)
289 uuid = String.split(tombstone.data["id"], "/") |> List.last()
290
291 conn =
292 conn
293 |> put_req_header("accept", "application/activity+json")
294 |> get("/objects/#{uuid}")
295
296 assert json_response(conn, 404)
297 end
298
299 test "it caches a response", %{conn: conn} do
300 note = insert(:note)
301 uuid = String.split(note.data["id"], "/") |> List.last()
302
303 conn1 =
304 conn
305 |> put_req_header("accept", "application/activity+json")
306 |> get("/objects/#{uuid}")
307
308 assert json_response(conn1, :ok)
309 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
310
311 conn2 =
312 conn
313 |> put_req_header("accept", "application/activity+json")
314 |> get("/objects/#{uuid}")
315
316 assert json_response(conn1, :ok) == json_response(conn2, :ok)
317 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
318 end
319
320 test "cached purged after object deletion", %{conn: conn} do
321 note = insert(:note)
322 uuid = String.split(note.data["id"], "/") |> List.last()
323
324 conn1 =
325 conn
326 |> put_req_header("accept", "application/activity+json")
327 |> get("/objects/#{uuid}")
328
329 assert json_response(conn1, :ok)
330 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
331
332 Object.delete(note)
333
334 conn2 =
335 conn
336 |> put_req_header("accept", "application/activity+json")
337 |> get("/objects/#{uuid}")
338
339 assert "Not found" == json_response(conn2, :not_found)
340 end
341
342 test "it requires authentication if instance is NOT federating", %{
343 conn: conn
344 } do
345 user = insert(:user)
346 note = insert(:note)
347 uuid = String.split(note.data["id"], "/") |> List.last()
348
349 conn = put_req_header(conn, "accept", "application/activity+json")
350
351 ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user)
352 end
353 end
354
355 describe "/activities/:uuid" do
356 test "it returns a json representation of the activity", %{conn: conn} do
357 activity = insert(:note_activity)
358 uuid = String.split(activity.data["id"], "/") |> List.last()
359
360 conn =
361 conn
362 |> put_req_header("accept", "application/activity+json")
363 |> get("/activities/#{uuid}")
364
365 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
366 end
367
368 test "it returns 404 for non-public activities", %{conn: conn} do
369 activity = insert(:direct_note_activity)
370 uuid = String.split(activity.data["id"], "/") |> List.last()
371
372 conn =
373 conn
374 |> put_req_header("accept", "application/activity+json")
375 |> get("/activities/#{uuid}")
376
377 assert json_response(conn, 404)
378 end
379
380 test "it caches a response", %{conn: conn} do
381 activity = insert(:note_activity)
382 uuid = String.split(activity.data["id"], "/") |> List.last()
383
384 conn1 =
385 conn
386 |> put_req_header("accept", "application/activity+json")
387 |> get("/activities/#{uuid}")
388
389 assert json_response(conn1, :ok)
390 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
391
392 conn2 =
393 conn
394 |> put_req_header("accept", "application/activity+json")
395 |> get("/activities/#{uuid}")
396
397 assert json_response(conn1, :ok) == json_response(conn2, :ok)
398 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
399 end
400
401 test "cached purged after activity deletion", %{conn: conn} do
402 user = insert(:user)
403 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
404
405 uuid = String.split(activity.data["id"], "/") |> List.last()
406
407 conn1 =
408 conn
409 |> put_req_header("accept", "application/activity+json")
410 |> get("/activities/#{uuid}")
411
412 assert json_response(conn1, :ok)
413 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
414
415 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
416
417 conn2 =
418 conn
419 |> put_req_header("accept", "application/activity+json")
420 |> get("/activities/#{uuid}")
421
422 assert "Not found" == json_response(conn2, :not_found)
423 end
424
425 test "it requires authentication if instance is NOT federating", %{
426 conn: conn
427 } do
428 user = insert(:user)
429 activity = insert(:note_activity)
430 uuid = String.split(activity.data["id"], "/") |> List.last()
431
432 conn = put_req_header(conn, "accept", "application/activity+json")
433
434 ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user)
435 end
436 end
437
438 describe "/inbox" do
439 test "it inserts an incoming activity into the database", %{conn: conn} do
440 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
441
442 conn =
443 conn
444 |> assign(:valid_signature, true)
445 |> put_req_header("content-type", "application/activity+json")
446 |> post("/inbox", data)
447
448 assert "ok" == json_response(conn, 200)
449
450 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
451 assert Activity.get_by_ap_id(data["id"])
452 end
453
454 @tag capture_log: true
455 test "it inserts an incoming activity into the database" <>
456 "even if we can't fetch the user but have it in our db",
457 %{conn: conn} do
458 user =
459 insert(:user,
460 ap_id: "https://mastodon.example.org/users/raymoo",
461 ap_enabled: true,
462 local: false,
463 last_refreshed_at: nil
464 )
465
466 data =
467 File.read!("test/fixtures/mastodon-post-activity.json")
468 |> Poison.decode!()
469 |> Map.put("actor", user.ap_id)
470 |> put_in(["object", "attridbutedTo"], user.ap_id)
471
472 conn =
473 conn
474 |> assign(:valid_signature, true)
475 |> put_req_header("content-type", "application/activity+json")
476 |> post("/inbox", data)
477
478 assert "ok" == json_response(conn, 200)
479
480 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
481 assert Activity.get_by_ap_id(data["id"])
482 end
483
484 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
485 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
486
487 sender_url = data["actor"]
488 Instances.set_consistently_unreachable(sender_url)
489 refute Instances.reachable?(sender_url)
490
491 conn =
492 conn
493 |> assign(:valid_signature, true)
494 |> put_req_header("content-type", "application/activity+json")
495 |> post("/inbox", data)
496
497 assert "ok" == json_response(conn, 200)
498 assert Instances.reachable?(sender_url)
499 end
500
501 test "accept follow activity", %{conn: conn} do
502 Pleroma.Config.put([:instance, :federating], true)
503 relay = Relay.get_actor()
504
505 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
506
507 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
508 relay = refresh_record(relay)
509
510 accept =
511 File.read!("test/fixtures/relay/accept-follow.json")
512 |> String.replace("{{ap_id}}", relay.ap_id)
513 |> String.replace("{{activity_id}}", activity.data["id"])
514
515 assert "ok" ==
516 conn
517 |> assign(:valid_signature, true)
518 |> put_req_header("content-type", "application/activity+json")
519 |> post("/inbox", accept)
520 |> json_response(200)
521
522 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
523
524 assert Pleroma.FollowingRelationship.following?(
525 relay,
526 followed_relay
527 )
528
529 Mix.shell(Mix.Shell.Process)
530
531 on_exit(fn ->
532 Mix.shell(Mix.Shell.IO)
533 end)
534
535 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
536 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
537 end
538
539 @tag capture_log: true
540 test "without valid signature, " <>
541 "it only accepts Create activities and requires enabled federation",
542 %{conn: conn} do
543 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
544 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
545
546 conn = put_req_header(conn, "content-type", "application/activity+json")
547
548 Config.put([:instance, :federating], false)
549
550 conn
551 |> post("/inbox", data)
552 |> json_response(403)
553
554 conn
555 |> post("/inbox", non_create_data)
556 |> json_response(403)
557
558 Config.put([:instance, :federating], true)
559
560 ret_conn = post(conn, "/inbox", data)
561 assert "ok" == json_response(ret_conn, 200)
562
563 conn
564 |> post("/inbox", non_create_data)
565 |> json_response(400)
566 end
567 end
568
569 describe "/users/:nickname/inbox" do
570 setup do
571 data =
572 File.read!("test/fixtures/mastodon-post-activity.json")
573 |> Poison.decode!()
574
575 [data: data]
576 end
577
578 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
579 user = insert(:user)
580 data = Map.put(data, "bcc", [user.ap_id])
581
582 conn =
583 conn
584 |> assign(:valid_signature, true)
585 |> put_req_header("content-type", "application/activity+json")
586 |> post("/users/#{user.nickname}/inbox", data)
587
588 assert "ok" == json_response(conn, 200)
589 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
590 assert Activity.get_by_ap_id(data["id"])
591 end
592
593 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
594 user = insert(:user)
595
596 data =
597 Map.put(data, "to", user.ap_id)
598 |> Map.delete("cc")
599
600 conn =
601 conn
602 |> assign(:valid_signature, true)
603 |> put_req_header("content-type", "application/activity+json")
604 |> post("/users/#{user.nickname}/inbox", data)
605
606 assert "ok" == json_response(conn, 200)
607 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
608 assert Activity.get_by_ap_id(data["id"])
609 end
610
611 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
612 user = insert(:user)
613
614 data =
615 Map.put(data, "cc", user.ap_id)
616 |> Map.delete("to")
617
618 conn =
619 conn
620 |> assign(:valid_signature, true)
621 |> put_req_header("content-type", "application/activity+json")
622 |> post("/users/#{user.nickname}/inbox", data)
623
624 assert "ok" == json_response(conn, 200)
625 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
626 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
627 assert user.ap_id in activity.recipients
628 end
629
630 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
631 user = insert(:user)
632
633 data =
634 Map.put(data, "bcc", user.ap_id)
635 |> Map.delete("to")
636 |> Map.delete("cc")
637
638 conn =
639 conn
640 |> assign(:valid_signature, true)
641 |> put_req_header("content-type", "application/activity+json")
642 |> post("/users/#{user.nickname}/inbox", data)
643
644 assert "ok" == json_response(conn, 200)
645 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
646 assert Activity.get_by_ap_id(data["id"])
647 end
648
649 test "it accepts announces with to as string instead of array", %{conn: conn} do
650 user = insert(:user)
651
652 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
653 announcer = insert(:user, local: false)
654
655 data = %{
656 "@context" => "https://www.w3.org/ns/activitystreams",
657 "actor" => announcer.ap_id,
658 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
659 "object" => post.data["object"],
660 "to" => "https://www.w3.org/ns/activitystreams#Public",
661 "cc" => [user.ap_id],
662 "type" => "Announce"
663 }
664
665 conn =
666 conn
667 |> assign(:valid_signature, true)
668 |> put_req_header("content-type", "application/activity+json")
669 |> post("/users/#{user.nickname}/inbox", data)
670
671 assert "ok" == json_response(conn, 200)
672 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
673 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
674 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
675 end
676
677 test "it accepts messages from actors that are followed by the user", %{
678 conn: conn,
679 data: data
680 } do
681 recipient = insert(:user)
682 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
683
684 {:ok, recipient} = User.follow(recipient, actor)
685
686 object =
687 data["object"]
688 |> Map.put("attributedTo", actor.ap_id)
689
690 data =
691 data
692 |> Map.put("actor", actor.ap_id)
693 |> Map.put("object", object)
694
695 conn =
696 conn
697 |> assign(:valid_signature, true)
698 |> put_req_header("content-type", "application/activity+json")
699 |> post("/users/#{recipient.nickname}/inbox", data)
700
701 assert "ok" == json_response(conn, 200)
702 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
703 assert Activity.get_by_ap_id(data["id"])
704 end
705
706 test "it rejects reads from other users", %{conn: conn} do
707 user = insert(:user)
708 other_user = insert(:user)
709
710 conn =
711 conn
712 |> assign(:user, other_user)
713 |> put_req_header("accept", "application/activity+json")
714 |> get("/users/#{user.nickname}/inbox")
715
716 assert json_response(conn, 403)
717 end
718
719 test "it returns a note activity in a collection", %{conn: conn} do
720 note_activity = insert(:direct_note_activity)
721 note_object = Object.normalize(note_activity)
722 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
723
724 conn =
725 conn
726 |> assign(:user, user)
727 |> put_req_header("accept", "application/activity+json")
728 |> get("/users/#{user.nickname}/inbox?page=true")
729
730 assert response(conn, 200) =~ note_object.data["content"]
731 end
732
733 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
734 user = insert(:user)
735 data = Map.put(data, "bcc", [user.ap_id])
736
737 sender_host = URI.parse(data["actor"]).host
738 Instances.set_consistently_unreachable(sender_host)
739 refute Instances.reachable?(sender_host)
740
741 conn =
742 conn
743 |> assign(:valid_signature, true)
744 |> put_req_header("content-type", "application/activity+json")
745 |> post("/users/#{user.nickname}/inbox", data)
746
747 assert "ok" == json_response(conn, 200)
748 assert Instances.reachable?(sender_host)
749 end
750
751 test "it removes all follower collections but actor's", %{conn: conn} do
752 [actor, recipient] = insert_pair(:user)
753
754 data =
755 File.read!("test/fixtures/activitypub-client-post-activity.json")
756 |> Poison.decode!()
757
758 object = Map.put(data["object"], "attributedTo", actor.ap_id)
759
760 data =
761 data
762 |> Map.put("id", Utils.generate_object_id())
763 |> Map.put("actor", actor.ap_id)
764 |> Map.put("object", object)
765 |> Map.put("cc", [
766 recipient.follower_address,
767 actor.follower_address
768 ])
769 |> Map.put("to", [
770 recipient.ap_id,
771 recipient.follower_address,
772 "https://www.w3.org/ns/activitystreams#Public"
773 ])
774
775 conn
776 |> assign(:valid_signature, true)
777 |> put_req_header("content-type", "application/activity+json")
778 |> post("/users/#{recipient.nickname}/inbox", data)
779 |> json_response(200)
780
781 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
782
783 activity = Activity.get_by_ap_id(data["id"])
784
785 assert activity.id
786 assert actor.follower_address in activity.recipients
787 assert actor.follower_address in activity.data["cc"]
788
789 refute recipient.follower_address in activity.recipients
790 refute recipient.follower_address in activity.data["cc"]
791 refute recipient.follower_address in activity.data["to"]
792 end
793
794 test "it requires authentication", %{conn: conn} do
795 user = insert(:user)
796 conn = put_req_header(conn, "accept", "application/activity+json")
797
798 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
799 assert json_response(ret_conn, 403)
800
801 ret_conn =
802 conn
803 |> assign(:user, user)
804 |> get("/users/#{user.nickname}/inbox")
805
806 assert json_response(ret_conn, 200)
807 end
808 end
809
810 describe "GET /users/:nickname/outbox" do
811 test "it paginates correctly", %{conn: conn} do
812 user = insert(:user)
813 conn = assign(conn, :user, user)
814 outbox_endpoint = user.ap_id <> "/outbox"
815
816 _posts =
817 for i <- 0..25 do
818 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
819 activity
820 end
821
822 result =
823 conn
824 |> put_req_header("accept", "application/activity+json")
825 |> get(outbox_endpoint <> "?page=true")
826 |> json_response(200)
827
828 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
829 assert length(result["orderedItems"]) == 20
830 assert length(result_ids) == 20
831 assert result["next"]
832 assert String.starts_with?(result["next"], outbox_endpoint)
833
834 result_next =
835 conn
836 |> put_req_header("accept", "application/activity+json")
837 |> get(result["next"])
838 |> json_response(200)
839
840 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
841 assert length(result_next["orderedItems"]) == 6
842 assert length(result_next_ids) == 6
843 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
844 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
845 assert String.starts_with?(result["id"], outbox_endpoint)
846
847 result_next_again =
848 conn
849 |> put_req_header("accept", "application/activity+json")
850 |> get(result_next["id"])
851 |> json_response(200)
852
853 assert result_next == result_next_again
854 end
855
856 test "it returns 200 even if there're no activities", %{conn: conn} do
857 user = insert(:user)
858 outbox_endpoint = user.ap_id <> "/outbox"
859
860 conn =
861 conn
862 |> assign(:user, user)
863 |> put_req_header("accept", "application/activity+json")
864 |> get(outbox_endpoint)
865
866 result = json_response(conn, 200)
867 assert outbox_endpoint == result["id"]
868 end
869
870 test "it returns a note activity in a collection", %{conn: conn} do
871 note_activity = insert(:note_activity)
872 note_object = Object.normalize(note_activity)
873 user = User.get_cached_by_ap_id(note_activity.data["actor"])
874
875 conn =
876 conn
877 |> assign(:user, user)
878 |> put_req_header("accept", "application/activity+json")
879 |> get("/users/#{user.nickname}/outbox?page=true")
880
881 assert response(conn, 200) =~ note_object.data["content"]
882 end
883
884 test "it returns an announce activity in a collection", %{conn: conn} do
885 announce_activity = insert(:announce_activity)
886 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
887
888 conn =
889 conn
890 |> assign(:user, user)
891 |> put_req_header("accept", "application/activity+json")
892 |> get("/users/#{user.nickname}/outbox?page=true")
893
894 assert response(conn, 200) =~ announce_activity.data["object"]
895 end
896
897 test "it requires authentication if instance is NOT federating", %{
898 conn: conn
899 } do
900 user = insert(:user)
901 conn = put_req_header(conn, "accept", "application/activity+json")
902
903 ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user)
904 end
905 end
906
907 describe "POST /users/:nickname/outbox (C2S)" do
908 setup do: clear_config([:instance, :limit])
909
910 setup do
911 [
912 activity: %{
913 "@context" => "https://www.w3.org/ns/activitystreams",
914 "type" => "Create",
915 "object" => %{"type" => "Note", "content" => "AP C2S test"},
916 "to" => "https://www.w3.org/ns/activitystreams#Public",
917 "cc" => []
918 }
919 ]
920 end
921
922 test "it rejects posts from other users / unauthenticated users", %{
923 conn: conn,
924 activity: activity
925 } do
926 user = insert(:user)
927 other_user = insert(:user)
928 conn = put_req_header(conn, "content-type", "application/activity+json")
929
930 conn
931 |> post("/users/#{user.nickname}/outbox", activity)
932 |> json_response(403)
933
934 conn
935 |> assign(:user, other_user)
936 |> post("/users/#{user.nickname}/outbox", activity)
937 |> json_response(403)
938 end
939
940 test "it inserts an incoming create activity into the database", %{
941 conn: conn,
942 activity: activity
943 } do
944 user = insert(:user)
945
946 result =
947 conn
948 |> assign(:user, user)
949 |> put_req_header("content-type", "application/activity+json")
950 |> post("/users/#{user.nickname}/outbox", activity)
951 |> json_response(201)
952
953 assert Activity.get_by_ap_id(result["id"])
954 assert result["object"]
955 assert %Object{data: object} = Object.normalize(result["object"])
956 assert object["content"] == activity["object"]["content"]
957 end
958
959 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
960 user = insert(:user)
961
962 activity =
963 activity
964 |> put_in(["object", "type"], "Benis")
965
966 _result =
967 conn
968 |> assign(:user, user)
969 |> put_req_header("content-type", "application/activity+json")
970 |> post("/users/#{user.nickname}/outbox", activity)
971 |> json_response(400)
972 end
973
974 test "it inserts an incoming sensitive activity into the database", %{
975 conn: conn,
976 activity: activity
977 } do
978 user = insert(:user)
979 conn = assign(conn, :user, user)
980 object = Map.put(activity["object"], "sensitive", true)
981 activity = Map.put(activity, "object", object)
982
983 response =
984 conn
985 |> put_req_header("content-type", "application/activity+json")
986 |> post("/users/#{user.nickname}/outbox", activity)
987 |> json_response(201)
988
989 assert Activity.get_by_ap_id(response["id"])
990 assert response["object"]
991 assert %Object{data: response_object} = Object.normalize(response["object"])
992 assert response_object["sensitive"] == true
993 assert response_object["content"] == activity["object"]["content"]
994
995 representation =
996 conn
997 |> put_req_header("accept", "application/activity+json")
998 |> get(response["id"])
999 |> json_response(200)
1000
1001 assert representation["object"]["sensitive"] == true
1002 end
1003
1004 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1005 user = insert(:user)
1006 activity = Map.put(activity, "type", "BadType")
1007
1008 conn =
1009 conn
1010 |> assign(:user, user)
1011 |> put_req_header("content-type", "application/activity+json")
1012 |> post("/users/#{user.nickname}/outbox", activity)
1013
1014 assert json_response(conn, 400)
1015 end
1016
1017 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1018 note_activity = insert(:note_activity)
1019 note_object = Object.normalize(note_activity)
1020 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1021
1022 data = %{
1023 type: "Delete",
1024 object: %{
1025 id: note_object.data["id"]
1026 }
1027 }
1028
1029 conn =
1030 conn
1031 |> assign(:user, user)
1032 |> put_req_header("content-type", "application/activity+json")
1033 |> post("/users/#{user.nickname}/outbox", data)
1034
1035 result = json_response(conn, 201)
1036 assert Activity.get_by_ap_id(result["id"])
1037
1038 assert object = Object.get_by_ap_id(note_object.data["id"])
1039 assert object.data["type"] == "Tombstone"
1040 end
1041
1042 test "it rejects delete activity of object from other actor", %{conn: conn} do
1043 note_activity = insert(:note_activity)
1044 note_object = Object.normalize(note_activity)
1045 user = insert(:user)
1046
1047 data = %{
1048 type: "Delete",
1049 object: %{
1050 id: note_object.data["id"]
1051 }
1052 }
1053
1054 conn =
1055 conn
1056 |> assign(:user, user)
1057 |> put_req_header("content-type", "application/activity+json")
1058 |> post("/users/#{user.nickname}/outbox", data)
1059
1060 assert json_response(conn, 400)
1061 end
1062
1063 test "it increases like count when receiving a like action", %{conn: conn} do
1064 note_activity = insert(:note_activity)
1065 note_object = Object.normalize(note_activity)
1066 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1067
1068 data = %{
1069 type: "Like",
1070 object: %{
1071 id: note_object.data["id"]
1072 }
1073 }
1074
1075 conn =
1076 conn
1077 |> assign(:user, user)
1078 |> put_req_header("content-type", "application/activity+json")
1079 |> post("/users/#{user.nickname}/outbox", data)
1080
1081 result = json_response(conn, 201)
1082 assert Activity.get_by_ap_id(result["id"])
1083
1084 assert object = Object.get_by_ap_id(note_object.data["id"])
1085 assert object.data["like_count"] == 1
1086 end
1087
1088 test "it doesn't spreads faulty attributedTo or actor fields", %{
1089 conn: conn,
1090 activity: activity
1091 } do
1092 reimu = insert(:user, nickname: "reimu")
1093 cirno = insert(:user, nickname: "cirno")
1094
1095 assert reimu.ap_id
1096 assert cirno.ap_id
1097
1098 activity =
1099 activity
1100 |> put_in(["object", "actor"], reimu.ap_id)
1101 |> put_in(["object", "attributedTo"], reimu.ap_id)
1102 |> put_in(["actor"], reimu.ap_id)
1103 |> put_in(["attributedTo"], reimu.ap_id)
1104
1105 _reimu_outbox =
1106 conn
1107 |> assign(:user, cirno)
1108 |> put_req_header("content-type", "application/activity+json")
1109 |> post("/users/#{reimu.nickname}/outbox", activity)
1110 |> json_response(403)
1111
1112 cirno_outbox =
1113 conn
1114 |> assign(:user, cirno)
1115 |> put_req_header("content-type", "application/activity+json")
1116 |> post("/users/#{cirno.nickname}/outbox", activity)
1117 |> json_response(201)
1118
1119 assert cirno_outbox["attributedTo"] == nil
1120 assert cirno_outbox["actor"] == cirno.ap_id
1121
1122 assert cirno_object = Object.normalize(cirno_outbox["object"])
1123 assert cirno_object.data["actor"] == cirno.ap_id
1124 assert cirno_object.data["attributedTo"] == cirno.ap_id
1125 end
1126
1127 test "Character limitation", %{conn: conn, activity: activity} do
1128 Pleroma.Config.put([:instance, :limit], 5)
1129 user = insert(:user)
1130
1131 result =
1132 conn
1133 |> assign(:user, user)
1134 |> put_req_header("content-type", "application/activity+json")
1135 |> post("/users/#{user.nickname}/outbox", activity)
1136 |> json_response(400)
1137
1138 assert result == "Note is over the character limit"
1139 end
1140 end
1141
1142 describe "/relay/followers" do
1143 test "it returns relay followers", %{conn: conn} do
1144 relay_actor = Relay.get_actor()
1145 user = insert(:user)
1146 User.follow(user, relay_actor)
1147
1148 result =
1149 conn
1150 |> get("/relay/followers")
1151 |> json_response(200)
1152
1153 assert result["first"]["orderedItems"] == [user.ap_id]
1154 end
1155
1156 test "on non-federating instance, it returns 404", %{conn: conn} do
1157 Config.put([:instance, :federating], false)
1158 user = insert(:user)
1159
1160 conn
1161 |> assign(:user, user)
1162 |> get("/relay/followers")
1163 |> json_response(404)
1164 end
1165 end
1166
1167 describe "/relay/following" do
1168 test "it returns relay following", %{conn: conn} do
1169 result =
1170 conn
1171 |> get("/relay/following")
1172 |> json_response(200)
1173
1174 assert result["first"]["orderedItems"] == []
1175 end
1176
1177 test "on non-federating instance, it returns 404", %{conn: conn} do
1178 Config.put([:instance, :federating], false)
1179 user = insert(:user)
1180
1181 conn
1182 |> assign(:user, user)
1183 |> get("/relay/following")
1184 |> json_response(404)
1185 end
1186 end
1187
1188 describe "/users/:nickname/followers" do
1189 test "it returns the followers in a collection", %{conn: conn} do
1190 user = insert(:user)
1191 user_two = insert(:user)
1192 User.follow(user, user_two)
1193
1194 result =
1195 conn
1196 |> assign(:user, user_two)
1197 |> get("/users/#{user_two.nickname}/followers")
1198 |> json_response(200)
1199
1200 assert result["first"]["orderedItems"] == [user.ap_id]
1201 end
1202
1203 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1204 user = insert(:user)
1205 user_two = insert(:user, hide_followers: true)
1206 User.follow(user, user_two)
1207
1208 result =
1209 conn
1210 |> assign(:user, user)
1211 |> get("/users/#{user_two.nickname}/followers")
1212 |> json_response(200)
1213
1214 assert is_binary(result["first"])
1215 end
1216
1217 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1218 %{conn: conn} do
1219 user = insert(:user)
1220 other_user = insert(:user, hide_followers: true)
1221
1222 result =
1223 conn
1224 |> assign(:user, user)
1225 |> get("/users/#{other_user.nickname}/followers?page=1")
1226
1227 assert result.status == 403
1228 assert result.resp_body == ""
1229 end
1230
1231 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1232 %{conn: conn} do
1233 user = insert(:user, hide_followers: true)
1234 other_user = insert(:user)
1235 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1236
1237 result =
1238 conn
1239 |> assign(:user, user)
1240 |> get("/users/#{user.nickname}/followers?page=1")
1241 |> json_response(200)
1242
1243 assert result["totalItems"] == 1
1244 assert result["orderedItems"] == [other_user.ap_id]
1245 end
1246
1247 test "it works for more than 10 users", %{conn: conn} do
1248 user = insert(:user)
1249
1250 Enum.each(1..15, fn _ ->
1251 other_user = insert(:user)
1252 User.follow(other_user, user)
1253 end)
1254
1255 result =
1256 conn
1257 |> assign(:user, user)
1258 |> get("/users/#{user.nickname}/followers")
1259 |> json_response(200)
1260
1261 assert length(result["first"]["orderedItems"]) == 10
1262 assert result["first"]["totalItems"] == 15
1263 assert result["totalItems"] == 15
1264
1265 result =
1266 conn
1267 |> assign(:user, user)
1268 |> get("/users/#{user.nickname}/followers?page=2")
1269 |> json_response(200)
1270
1271 assert length(result["orderedItems"]) == 5
1272 assert result["totalItems"] == 15
1273 end
1274
1275 test "does not require authentication", %{conn: conn} do
1276 user = insert(:user)
1277
1278 conn
1279 |> get("/users/#{user.nickname}/followers")
1280 |> json_response(200)
1281 end
1282 end
1283
1284 describe "/users/:nickname/following" do
1285 test "it returns the following in a collection", %{conn: conn} do
1286 user = insert(:user)
1287 user_two = insert(:user)
1288 User.follow(user, user_two)
1289
1290 result =
1291 conn
1292 |> assign(:user, user)
1293 |> get("/users/#{user.nickname}/following")
1294 |> json_response(200)
1295
1296 assert result["first"]["orderedItems"] == [user_two.ap_id]
1297 end
1298
1299 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1300 user = insert(:user)
1301 user_two = insert(:user, hide_follows: true)
1302 User.follow(user, user_two)
1303
1304 result =
1305 conn
1306 |> assign(:user, user)
1307 |> get("/users/#{user_two.nickname}/following")
1308 |> json_response(200)
1309
1310 assert is_binary(result["first"])
1311 end
1312
1313 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1314 %{conn: conn} do
1315 user = insert(:user)
1316 user_two = insert(:user, hide_follows: true)
1317
1318 result =
1319 conn
1320 |> assign(:user, user)
1321 |> get("/users/#{user_two.nickname}/following?page=1")
1322
1323 assert result.status == 403
1324 assert result.resp_body == ""
1325 end
1326
1327 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1328 %{conn: conn} do
1329 user = insert(:user, hide_follows: true)
1330 other_user = insert(:user)
1331 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1332
1333 result =
1334 conn
1335 |> assign(:user, user)
1336 |> get("/users/#{user.nickname}/following?page=1")
1337 |> json_response(200)
1338
1339 assert result["totalItems"] == 1
1340 assert result["orderedItems"] == [other_user.ap_id]
1341 end
1342
1343 test "it works for more than 10 users", %{conn: conn} do
1344 user = insert(:user)
1345
1346 Enum.each(1..15, fn _ ->
1347 user = User.get_cached_by_id(user.id)
1348 other_user = insert(:user)
1349 User.follow(user, other_user)
1350 end)
1351
1352 result =
1353 conn
1354 |> assign(:user, user)
1355 |> get("/users/#{user.nickname}/following")
1356 |> json_response(200)
1357
1358 assert length(result["first"]["orderedItems"]) == 10
1359 assert result["first"]["totalItems"] == 15
1360 assert result["totalItems"] == 15
1361
1362 result =
1363 conn
1364 |> assign(:user, user)
1365 |> get("/users/#{user.nickname}/following?page=2")
1366 |> json_response(200)
1367
1368 assert length(result["orderedItems"]) == 5
1369 assert result["totalItems"] == 15
1370 end
1371
1372 test "does not require authentication", %{conn: conn} do
1373 user = insert(:user)
1374
1375 conn
1376 |> get("/users/#{user.nickname}/following")
1377 |> json_response(200)
1378 end
1379 end
1380
1381 describe "delivery tracking" do
1382 test "it tracks a signed object fetch", %{conn: conn} do
1383 user = insert(:user, local: false)
1384 activity = insert(:note_activity)
1385 object = Object.normalize(activity)
1386
1387 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1388
1389 conn
1390 |> put_req_header("accept", "application/activity+json")
1391 |> assign(:user, user)
1392 |> get(object_path)
1393 |> json_response(200)
1394
1395 assert Delivery.get(object.id, user.id)
1396 end
1397
1398 test "it tracks a signed activity fetch", %{conn: conn} do
1399 user = insert(:user, local: false)
1400 activity = insert(:note_activity)
1401 object = Object.normalize(activity)
1402
1403 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1404
1405 conn
1406 |> put_req_header("accept", "application/activity+json")
1407 |> assign(:user, user)
1408 |> get(activity_path)
1409 |> json_response(200)
1410
1411 assert Delivery.get(object.id, user.id)
1412 end
1413
1414 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1415 user = insert(:user, local: false)
1416 other_user = insert(:user, local: false)
1417 activity = insert(:note_activity)
1418 object = Object.normalize(activity)
1419
1420 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1421
1422 conn
1423 |> put_req_header("accept", "application/activity+json")
1424 |> assign(:user, user)
1425 |> get(object_path)
1426 |> json_response(200)
1427
1428 build_conn()
1429 |> put_req_header("accept", "application/activity+json")
1430 |> assign(:user, other_user)
1431 |> get(object_path)
1432 |> json_response(200)
1433
1434 assert Delivery.get(object.id, user.id)
1435 assert Delivery.get(object.id, other_user.id)
1436 end
1437
1438 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1439 user = insert(:user, local: false)
1440 other_user = insert(:user, local: false)
1441 activity = insert(:note_activity)
1442 object = Object.normalize(activity)
1443
1444 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1445
1446 conn
1447 |> put_req_header("accept", "application/activity+json")
1448 |> assign(:user, user)
1449 |> get(activity_path)
1450 |> json_response(200)
1451
1452 build_conn()
1453 |> put_req_header("accept", "application/activity+json")
1454 |> assign(:user, other_user)
1455 |> get(activity_path)
1456 |> json_response(200)
1457
1458 assert Delivery.get(object.id, user.id)
1459 assert Delivery.get(object.id, other_user.id)
1460 end
1461 end
1462
1463 describe "Additional ActivityPub C2S endpoints" do
1464 test "GET /api/ap/whoami", %{conn: conn} do
1465 user = insert(:user)
1466
1467 conn =
1468 conn
1469 |> assign(:user, user)
1470 |> get("/api/ap/whoami")
1471
1472 user = User.get_cached_by_id(user.id)
1473
1474 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1475
1476 conn
1477 |> get("/api/ap/whoami")
1478 |> json_response(403)
1479 end
1480
1481 setup do: clear_config([:media_proxy])
1482 setup do: clear_config([Pleroma.Upload])
1483
1484 test "POST /api/ap/upload_media", %{conn: conn} do
1485 user = insert(:user)
1486
1487 desc = "Description of the image"
1488
1489 image = %Plug.Upload{
1490 content_type: "bad/content-type",
1491 path: Path.absname("test/fixtures/image.jpg"),
1492 filename: "an_image.png"
1493 }
1494
1495 object =
1496 conn
1497 |> assign(:user, user)
1498 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1499 |> json_response(:created)
1500
1501 assert object["name"] == desc
1502 assert object["type"] == "Document"
1503 assert object["actor"] == user.ap_id
1504 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1505 assert is_binary(object_href)
1506 assert object_mediatype == "image/jpeg"
1507 assert String.ends_with?(object_href, ".jpg")
1508
1509 activity_request = %{
1510 "@context" => "https://www.w3.org/ns/activitystreams",
1511 "type" => "Create",
1512 "object" => %{
1513 "type" => "Note",
1514 "content" => "AP C2S test, attachment",
1515 "attachment" => [object]
1516 },
1517 "to" => "https://www.w3.org/ns/activitystreams#Public",
1518 "cc" => []
1519 }
1520
1521 activity_response =
1522 conn
1523 |> assign(:user, user)
1524 |> post("/users/#{user.nickname}/outbox", activity_request)
1525 |> json_response(:created)
1526
1527 assert activity_response["id"]
1528 assert activity_response["object"]
1529 assert activity_response["actor"] == user.ap_id
1530
1531 assert %Object{data: %{"attachment" => [attachment]}} =
1532 Object.normalize(activity_response["object"])
1533
1534 assert attachment["type"] == "Document"
1535 assert attachment["name"] == desc
1536
1537 assert [
1538 %{
1539 "href" => ^object_href,
1540 "type" => "Link",
1541 "mediaType" => ^object_mediatype
1542 }
1543 ] = attachment["url"]
1544
1545 # Fails if unauthenticated
1546 conn
1547 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1548 |> json_response(403)
1549 end
1550 end
1551 end