cherry-pick security from upstream
[akkoma] / test / pleroma / web / activity_pub / activity_pub_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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.Delivery
11 alias Pleroma.Instances
12 alias Pleroma.Object
13 alias Pleroma.Tests.ObanHelpers
14 alias Pleroma.User
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.ObjectView
17 alias Pleroma.Web.ActivityPub.Relay
18 alias Pleroma.Web.ActivityPub.UserView
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.Endpoint
22 alias Pleroma.Workers.ReceiverWorker
23
24 import Pleroma.Factory
25
26 require Pleroma.Constants
27
28 setup_all do
29 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
30 :ok
31 end
32
33 setup do: clear_config([:instance, :federating], true)
34
35 describe "/relay" do
36 setup do: clear_config([:instance, :allow_relay])
37
38 test "with the relay active, it returns the relay user", %{conn: conn} do
39 res =
40 conn
41 |> get(activity_pub_path(conn, :relay))
42 |> json_response(200)
43
44 assert res["id"] =~ "/relay"
45 end
46
47 test "with the relay disabled, it returns 404", %{conn: conn} do
48 clear_config([:instance, :allow_relay], false)
49
50 conn
51 |> get(activity_pub_path(conn, :relay))
52 |> json_response(404)
53 end
54
55 test "on non-federating instance, it returns 404", %{conn: conn} do
56 clear_config([:instance, :federating], false)
57 user = insert(:user)
58
59 conn
60 |> assign(:user, user)
61 |> get(activity_pub_path(conn, :relay))
62 |> json_response(404)
63 end
64 end
65
66 describe "/internal/fetch" do
67 test "it returns the internal fetch user", %{conn: conn} do
68 res =
69 conn
70 |> get(activity_pub_path(conn, :internal_fetch))
71 |> json_response(200)
72
73 assert res["id"] =~ "/fetch"
74 end
75
76 test "on non-federating instance, it returns 404", %{conn: conn} do
77 clear_config([:instance, :federating], false)
78 user = insert(:user)
79
80 conn
81 |> assign(:user, user)
82 |> get(activity_pub_path(conn, :internal_fetch))
83 |> json_response(404)
84 end
85 end
86
87 describe "/users/:nickname" do
88 test "it returns a json representation of the user with accept application/json", %{
89 conn: conn
90 } do
91 user = insert(:user)
92
93 conn =
94 conn
95 |> put_req_header("accept", "application/json")
96 |> get("/users/#{user.nickname}")
97
98 user = User.get_cached_by_id(user.id)
99
100 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
101 end
102
103 test "it returns a json representation of the user with accept application/activity+json", %{
104 conn: conn
105 } do
106 user = insert(:user)
107
108 conn =
109 conn
110 |> put_req_header("accept", "application/activity+json")
111 |> get("/users/#{user.nickname}")
112
113 user = User.get_cached_by_id(user.id)
114
115 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
116 end
117
118 test "it returns a json representation of the user with accept application/ld+json", %{
119 conn: conn
120 } do
121 user = insert(:user)
122
123 conn =
124 conn
125 |> put_req_header(
126 "accept",
127 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
128 )
129 |> get("/users/#{user.nickname}")
130
131 user = User.get_cached_by_id(user.id)
132
133 assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
134 end
135
136 test "it returns 404 for remote users", %{
137 conn: conn
138 } do
139 user = insert(:user, local: false, nickname: "remoteuser@example.com")
140
141 conn =
142 conn
143 |> put_req_header("accept", "application/json")
144 |> get("/users/#{user.nickname}.json")
145
146 assert json_response(conn, 404)
147 end
148
149 test "it returns error when user is not found", %{conn: conn} do
150 response =
151 conn
152 |> put_req_header("accept", "application/json")
153 |> get("/users/jimm")
154 |> json_response(404)
155
156 assert response == "Not found"
157 end
158 end
159
160 describe "mastodon compatibility routes" do
161 test "it returns a json representation of the object with accept application/json", %{
162 conn: conn
163 } do
164 {:ok, object} =
165 %{
166 "type" => "Note",
167 "content" => "hey",
168 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
169 "actor" => Endpoint.url() <> "/users/raymoo",
170 "to" => [Pleroma.Constants.as_public()]
171 }
172 |> Object.create()
173
174 conn =
175 conn
176 |> put_req_header("accept", "application/json")
177 |> get("/users/raymoo/statuses/999999999")
178
179 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
180 end
181
182 test "it returns a json representation of the activity with accept application/json", %{
183 conn: conn
184 } do
185 {:ok, object} =
186 %{
187 "type" => "Note",
188 "content" => "hey",
189 "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
190 "actor" => Endpoint.url() <> "/users/raymoo",
191 "to" => [Pleroma.Constants.as_public()]
192 }
193 |> Object.create()
194
195 {:ok, activity, _} =
196 %{
197 "id" => object.data["id"] <> "/activity",
198 "type" => "Create",
199 "object" => object.data["id"],
200 "actor" => object.data["actor"],
201 "to" => object.data["to"]
202 }
203 |> ActivityPub.persist(local: true)
204
205 conn =
206 conn
207 |> put_req_header("accept", "application/json")
208 |> get("/users/raymoo/statuses/999999999/activity")
209
210 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
211 end
212 end
213
214 describe "/objects/:uuid" do
215 test "it doesn't return a local-only object", %{conn: conn} do
216 user = insert(:user)
217 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
218
219 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
220
221 object = Object.normalize(post, fetch: false)
222 uuid = String.split(object.data["id"], "/") |> List.last()
223
224 conn =
225 conn
226 |> put_req_header("accept", "application/json")
227 |> get("/objects/#{uuid}")
228
229 assert json_response(conn, 404)
230 end
231
232 test "returns local-only objects when authenticated", %{conn: conn} do
233 user = insert(:user)
234 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
235
236 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
237
238 object = Object.normalize(post, fetch: false)
239 uuid = String.split(object.data["id"], "/") |> List.last()
240
241 assert response =
242 conn
243 |> assign(:user, user)
244 |> put_req_header("accept", "application/activity+json")
245 |> get("/objects/#{uuid}")
246
247 assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
248 end
249
250 test "it returns a json representation of the object with accept application/json", %{
251 conn: conn
252 } do
253 note = insert(:note)
254 uuid = String.split(note.data["id"], "/") |> List.last()
255
256 conn =
257 conn
258 |> put_req_header("accept", "application/json")
259 |> get("/objects/#{uuid}")
260
261 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
262 end
263
264 test "it returns a json representation of the object with accept application/activity+json",
265 %{conn: conn} do
266 note = insert(:note)
267 uuid = String.split(note.data["id"], "/") |> List.last()
268
269 conn =
270 conn
271 |> put_req_header("accept", "application/activity+json")
272 |> get("/objects/#{uuid}")
273
274 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
275 end
276
277 test "it returns a json representation of the object with accept application/ld+json", %{
278 conn: conn
279 } do
280 note = insert(:note)
281 uuid = String.split(note.data["id"], "/") |> List.last()
282
283 conn =
284 conn
285 |> put_req_header(
286 "accept",
287 "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
288 )
289 |> get("/objects/#{uuid}")
290
291 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
292 end
293
294 test "does not cache authenticated response", %{conn: conn} do
295 user = insert(:user)
296 reader = insert(:user)
297
298 {:ok, post} =
299 CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"})
300
301 object = Object.normalize(post, fetch: false)
302 uuid = String.split(object.data["id"], "/") |> List.last()
303
304 assert response =
305 conn
306 |> assign(:user, reader)
307 |> put_req_header("accept", "application/activity+json")
308 |> get("/objects/#{uuid}")
309
310 json_response(response, 200)
311
312 conn
313 |> put_req_header("accept", "application/activity+json")
314 |> get("/objects/#{uuid}")
315 |> json_response(404)
316 end
317
318 test "it returns 404 for non-public messages", %{conn: conn} do
319 note = insert(:direct_note)
320 uuid = String.split(note.data["id"], "/") |> List.last()
321
322 conn =
323 conn
324 |> put_req_header("accept", "application/activity+json")
325 |> get("/objects/#{uuid}")
326
327 assert json_response(conn, 404)
328 end
329
330 test "returns visible non-public messages when authenticated", %{conn: conn} do
331 note = insert(:direct_note)
332 uuid = String.split(note.data["id"], "/") |> List.last()
333 user = User.get_by_ap_id(note.data["actor"])
334 marisa = insert(:user)
335
336 assert conn
337 |> assign(:user, marisa)
338 |> put_req_header("accept", "application/activity+json")
339 |> get("/objects/#{uuid}")
340 |> json_response(404)
341
342 assert response =
343 conn
344 |> assign(:user, user)
345 |> put_req_header("accept", "application/activity+json")
346 |> get("/objects/#{uuid}")
347 |> json_response(200)
348
349 assert response == ObjectView.render("object.json", %{object: note})
350 end
351
352 test "it returns 404 for tombstone objects", %{conn: conn} do
353 tombstone = insert(:tombstone)
354 uuid = String.split(tombstone.data["id"], "/") |> List.last()
355
356 conn =
357 conn
358 |> put_req_header("accept", "application/activity+json")
359 |> get("/objects/#{uuid}")
360
361 assert json_response(conn, 404)
362 end
363
364 test "it caches a response", %{conn: conn} do
365 note = insert(:note)
366 uuid = String.split(note.data["id"], "/") |> List.last()
367
368 conn1 =
369 conn
370 |> put_req_header("accept", "application/activity+json")
371 |> get("/objects/#{uuid}")
372
373 assert json_response(conn1, :ok)
374 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
375
376 conn2 =
377 conn
378 |> put_req_header("accept", "application/activity+json")
379 |> get("/objects/#{uuid}")
380
381 assert json_response(conn1, :ok) == json_response(conn2, :ok)
382 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
383 end
384
385 test "cached purged after object deletion", %{conn: conn} do
386 note = insert(:note)
387 uuid = String.split(note.data["id"], "/") |> List.last()
388
389 conn1 =
390 conn
391 |> put_req_header("accept", "application/activity+json")
392 |> get("/objects/#{uuid}")
393
394 assert json_response(conn1, :ok)
395 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
396
397 Object.delete(note)
398
399 conn2 =
400 conn
401 |> put_req_header("accept", "application/activity+json")
402 |> get("/objects/#{uuid}")
403
404 assert "Not found" == json_response(conn2, :not_found)
405 end
406 end
407
408 describe "/activities/:uuid" do
409 test "it doesn't return a local-only activity", %{conn: conn} do
410 user = insert(:user)
411 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
412
413 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
414
415 uuid = String.split(post.data["id"], "/") |> List.last()
416
417 conn =
418 conn
419 |> put_req_header("accept", "application/json")
420 |> get("/activities/#{uuid}")
421
422 assert json_response(conn, 404)
423 end
424
425 test "returns local-only activities when authenticated", %{conn: conn} do
426 user = insert(:user)
427 {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
428
429 assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
430
431 uuid = String.split(post.data["id"], "/") |> List.last()
432
433 assert response =
434 conn
435 |> assign(:user, user)
436 |> put_req_header("accept", "application/activity+json")
437 |> get("/activities/#{uuid}")
438
439 assert json_response(response, 200) == ObjectView.render("object.json", %{object: post})
440 end
441
442 test "it returns a json representation of the activity", %{conn: conn} do
443 activity = insert(:note_activity)
444 uuid = String.split(activity.data["id"], "/") |> List.last()
445
446 conn =
447 conn
448 |> put_req_header("accept", "application/activity+json")
449 |> get("/activities/#{uuid}")
450
451 assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
452 end
453
454 test "it returns 404 for non-public activities", %{conn: conn} do
455 activity = insert(:direct_note_activity)
456 uuid = String.split(activity.data["id"], "/") |> List.last()
457
458 conn =
459 conn
460 |> put_req_header("accept", "application/activity+json")
461 |> get("/activities/#{uuid}")
462
463 assert json_response(conn, 404)
464 end
465
466 test "returns visible non-public messages when authenticated", %{conn: conn} do
467 note = insert(:direct_note_activity)
468 uuid = String.split(note.data["id"], "/") |> List.last()
469 user = User.get_by_ap_id(note.data["actor"])
470 marisa = insert(:user)
471
472 assert conn
473 |> assign(:user, marisa)
474 |> put_req_header("accept", "application/activity+json")
475 |> get("/activities/#{uuid}")
476 |> json_response(404)
477
478 assert response =
479 conn
480 |> assign(:user, user)
481 |> put_req_header("accept", "application/activity+json")
482 |> get("/activities/#{uuid}")
483 |> json_response(200)
484
485 assert response == ObjectView.render("object.json", %{object: note})
486 end
487
488 test "it caches a response", %{conn: conn} do
489 activity = insert(:note_activity)
490 uuid = String.split(activity.data["id"], "/") |> List.last()
491
492 conn1 =
493 conn
494 |> put_req_header("accept", "application/activity+json")
495 |> get("/activities/#{uuid}")
496
497 assert json_response(conn1, :ok)
498 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
499
500 conn2 =
501 conn
502 |> put_req_header("accept", "application/activity+json")
503 |> get("/activities/#{uuid}")
504
505 assert json_response(conn1, :ok) == json_response(conn2, :ok)
506 assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
507 end
508
509 test "cached purged after activity deletion", %{conn: conn} do
510 user = insert(:user)
511 {:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
512
513 uuid = String.split(activity.data["id"], "/") |> List.last()
514
515 conn1 =
516 conn
517 |> put_req_header("accept", "application/activity+json")
518 |> get("/activities/#{uuid}")
519
520 assert json_response(conn1, :ok)
521 assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
522
523 Activity.delete_all_by_object_ap_id(activity.object.data["id"])
524
525 conn2 =
526 conn
527 |> put_req_header("accept", "application/activity+json")
528 |> get("/activities/#{uuid}")
529
530 assert "Not found" == json_response(conn2, :not_found)
531 end
532 end
533
534 describe "/inbox" do
535 test "it inserts an incoming activity into the database", %{conn: conn} do
536 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
537
538 conn =
539 conn
540 |> assign(:valid_signature, true)
541 |> put_req_header("content-type", "application/activity+json")
542 |> post("/inbox", data)
543
544 assert "ok" == json_response(conn, 200)
545
546 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
547 assert Activity.get_by_ap_id(data["id"])
548 end
549
550 @tag capture_log: true
551 test "it inserts an incoming activity into the database" <>
552 "even if we can't fetch the user but have it in our db",
553 %{conn: conn} do
554 user =
555 insert(:user,
556 ap_id: "https://mastodon.example.org/users/raymoo",
557 ap_enabled: true,
558 local: false,
559 last_refreshed_at: nil
560 )
561
562 data =
563 File.read!("test/fixtures/mastodon-post-activity.json")
564 |> Jason.decode!()
565 |> Map.put("actor", user.ap_id)
566 |> put_in(["object", "attributedTo"], user.ap_id)
567
568 conn =
569 conn
570 |> assign(:valid_signature, true)
571 |> put_req_header("content-type", "application/activity+json")
572 |> post("/inbox", data)
573
574 assert "ok" == json_response(conn, 200)
575
576 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
577 assert Activity.get_by_ap_id(data["id"])
578 end
579
580 test "it clears `unreachable` federation status of the sender", %{conn: conn} do
581 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
582
583 sender_url = data["actor"]
584 Instances.set_consistently_unreachable(sender_url)
585 refute Instances.reachable?(sender_url)
586
587 conn =
588 conn
589 |> assign(:valid_signature, true)
590 |> put_req_header("content-type", "application/activity+json")
591 |> post("/inbox", data)
592
593 assert "ok" == json_response(conn, 200)
594 assert Instances.reachable?(sender_url)
595 end
596
597 test "accept follow activity", %{conn: conn} do
598 clear_config([:instance, :federating], true)
599 relay = Relay.get_actor()
600
601 assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
602
603 followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
604 relay = refresh_record(relay)
605
606 accept =
607 File.read!("test/fixtures/relay/accept-follow.json")
608 |> String.replace("{{ap_id}}", relay.ap_id)
609 |> String.replace("{{activity_id}}", activity.data["id"])
610
611 assert "ok" ==
612 conn
613 |> assign(:valid_signature, true)
614 |> put_req_header("content-type", "application/activity+json")
615 |> post("/inbox", accept)
616 |> json_response(200)
617
618 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
619
620 assert Pleroma.FollowingRelationship.following?(
621 relay,
622 followed_relay
623 )
624
625 Mix.shell(Mix.Shell.Process)
626
627 on_exit(fn ->
628 Mix.shell(Mix.Shell.IO)
629 end)
630
631 :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
632 assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
633 end
634
635 @tag capture_log: true
636 test "without valid signature, " <>
637 "it only accepts Create activities and requires enabled federation",
638 %{conn: conn} do
639 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
640 non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
641
642 conn = put_req_header(conn, "content-type", "application/activity+json")
643
644 clear_config([:instance, :federating], false)
645
646 conn
647 |> post("/inbox", data)
648 |> json_response(403)
649
650 conn
651 |> post("/inbox", non_create_data)
652 |> json_response(403)
653
654 clear_config([:instance, :federating], true)
655
656 ret_conn = post(conn, "/inbox", data)
657 assert "ok" == json_response(ret_conn, 200)
658
659 conn
660 |> post("/inbox", non_create_data)
661 |> json_response(400)
662 end
663
664 test "accepts Add/Remove activities", %{conn: conn} do
665 object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
666
667 status =
668 File.read!("test/fixtures/statuses/note.json")
669 |> String.replace("{{nickname}}", "lain")
670 |> String.replace("{{object_id}}", object_id)
671
672 object_url = "https://example.com/objects/#{object_id}"
673
674 user =
675 File.read!("test/fixtures/users_mock/user.json")
676 |> String.replace("{{nickname}}", "lain")
677
678 actor = "https://example.com/users/lain"
679
680 Tesla.Mock.mock(fn
681 %{
682 method: :get,
683 url: ^object_url
684 } ->
685 %Tesla.Env{
686 status: 200,
687 body: status,
688 headers: [{"content-type", "application/activity+json"}]
689 }
690
691 %{
692 method: :get,
693 url: ^actor
694 } ->
695 %Tesla.Env{
696 status: 200,
697 body: user,
698 headers: [{"content-type", "application/activity+json"}]
699 }
700
701 %{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
702 %Tesla.Env{
703 status: 200,
704 body:
705 "test/fixtures/users_mock/masto_featured.json"
706 |> File.read!()
707 |> String.replace("{{domain}}", "example.com")
708 |> String.replace("{{nickname}}", "lain"),
709 headers: [{"content-type", "application/activity+json"}]
710 }
711 end)
712
713 data = %{
714 "id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423f",
715 "actor" => actor,
716 "object" => object_url,
717 "target" => "https://example.com/users/lain/collections/featured",
718 "type" => "Add",
719 "to" => [Pleroma.Constants.as_public()]
720 }
721
722 assert "ok" ==
723 conn
724 |> assign(:valid_signature, true)
725 |> put_req_header("content-type", "application/activity+json")
726 |> post("/inbox", data)
727 |> json_response(200)
728
729 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
730 assert Activity.get_by_ap_id(data["id"])
731 user = User.get_cached_by_ap_id(data["actor"])
732 assert user.pinned_objects[data["object"]]
733
734 data = %{
735 "id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423d",
736 "actor" => actor,
737 "object" => object_url,
738 "target" => "https://example.com/users/lain/collections/featured",
739 "type" => "Remove",
740 "to" => [Pleroma.Constants.as_public()]
741 }
742
743 assert "ok" ==
744 conn
745 |> assign(:valid_signature, true)
746 |> put_req_header("content-type", "application/activity+json")
747 |> post("/inbox", data)
748 |> json_response(200)
749
750 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
751 user = refresh_record(user)
752 refute user.pinned_objects[data["object"]]
753 end
754
755 test "mastodon pin/unpin", %{conn: conn} do
756 status_id = "105786274556060421"
757
758 status =
759 File.read!("test/fixtures/statuses/masto-note.json")
760 |> String.replace("{{nickname}}", "lain")
761 |> String.replace("{{status_id}}", status_id)
762
763 status_url = "https://example.com/users/lain/statuses/#{status_id}"
764
765 user =
766 File.read!("test/fixtures/users_mock/user.json")
767 |> String.replace("{{nickname}}", "lain")
768
769 actor = "https://example.com/users/lain"
770
771 Tesla.Mock.mock(fn
772 %{
773 method: :get,
774 url: ^status_url
775 } ->
776 %Tesla.Env{
777 status: 200,
778 body: status,
779 headers: [{"content-type", "application/activity+json"}]
780 }
781
782 %{
783 method: :get,
784 url: ^actor
785 } ->
786 %Tesla.Env{
787 status: 200,
788 body: user,
789 headers: [{"content-type", "application/activity+json"}]
790 }
791
792 %{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
793 %Tesla.Env{
794 status: 200,
795 body:
796 "test/fixtures/users_mock/masto_featured.json"
797 |> File.read!()
798 |> String.replace("{{domain}}", "example.com")
799 |> String.replace("{{nickname}}", "lain"),
800 headers: [{"content-type", "application/activity+json"}]
801 }
802 end)
803
804 data = %{
805 "@context" => "https://www.w3.org/ns/activitystreams",
806 "actor" => actor,
807 "object" => status_url,
808 "target" => "https://example.com/users/lain/collections/featured",
809 "type" => "Add"
810 }
811
812 assert "ok" ==
813 conn
814 |> assign(:valid_signature, true)
815 |> put_req_header("content-type", "application/activity+json")
816 |> post("/inbox", data)
817 |> json_response(200)
818
819 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
820 assert Activity.get_by_object_ap_id_with_object(data["object"])
821 user = User.get_cached_by_ap_id(data["actor"])
822 assert user.pinned_objects[data["object"]]
823
824 data = %{
825 "actor" => actor,
826 "object" => status_url,
827 "target" => "https://example.com/users/lain/collections/featured",
828 "type" => "Remove"
829 }
830
831 assert "ok" ==
832 conn
833 |> assign(:valid_signature, true)
834 |> put_req_header("content-type", "application/activity+json")
835 |> post("/inbox", data)
836 |> json_response(200)
837
838 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
839 assert Activity.get_by_object_ap_id_with_object(data["object"])
840 user = refresh_record(user)
841 refute user.pinned_objects[data["object"]]
842 end
843 end
844
845 describe "/users/:nickname/inbox" do
846 setup do
847 data =
848 File.read!("test/fixtures/mastodon-post-activity.json")
849 |> Jason.decode!()
850
851 [data: data]
852 end
853
854 test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
855 user = insert(:user)
856
857 data =
858 data
859 |> Map.put("bcc", [user.ap_id])
860 |> Kernel.put_in(["object", "bcc"], [user.ap_id])
861
862 conn =
863 conn
864 |> assign(:valid_signature, true)
865 |> put_req_header("content-type", "application/activity+json")
866 |> post("/users/#{user.nickname}/inbox", data)
867
868 assert "ok" == json_response(conn, 200)
869 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
870 assert Activity.get_by_ap_id(data["id"])
871 end
872
873 test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
874 user = insert(:user)
875
876 data =
877 data
878 |> Map.put("to", user.ap_id)
879 |> Map.put("cc", [])
880 |> Kernel.put_in(["object", "to"], user.ap_id)
881 |> Kernel.put_in(["object", "cc"], [])
882
883 conn =
884 conn
885 |> assign(:valid_signature, true)
886 |> put_req_header("content-type", "application/activity+json")
887 |> post("/users/#{user.nickname}/inbox", data)
888
889 assert "ok" == json_response(conn, 200)
890 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
891 assert Activity.get_by_ap_id(data["id"])
892 end
893
894 test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
895 user = insert(:user)
896
897 data =
898 data
899 |> Map.put("to", [])
900 |> Map.put("cc", user.ap_id)
901 |> Kernel.put_in(["object", "to"], [])
902 |> Kernel.put_in(["object", "cc"], user.ap_id)
903
904 conn =
905 conn
906 |> assign(:valid_signature, true)
907 |> put_req_header("content-type", "application/activity+json")
908 |> post("/users/#{user.nickname}/inbox", data)
909
910 assert "ok" == json_response(conn, 200)
911 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
912 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
913 assert user.ap_id in activity.recipients
914 end
915
916 test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
917 user = insert(:user)
918
919 data =
920 data
921 |> Map.put("to", [])
922 |> Map.put("cc", [])
923 |> Map.put("bcc", user.ap_id)
924 |> Kernel.put_in(["object", "to"], [])
925 |> Kernel.put_in(["object", "cc"], [])
926 |> Kernel.put_in(["object", "bcc"], user.ap_id)
927
928 conn =
929 conn
930 |> assign(:valid_signature, true)
931 |> put_req_header("content-type", "application/activity+json")
932 |> post("/users/#{user.nickname}/inbox", data)
933
934 assert "ok" == json_response(conn, 200)
935 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
936 assert Activity.get_by_ap_id(data["id"])
937 end
938
939 test "it accepts announces with to as string instead of array", %{conn: conn} do
940 user = insert(:user)
941
942 {:ok, post} = CommonAPI.post(user, %{status: "hey"})
943 announcer = insert(:user, local: false)
944
945 data = %{
946 "@context" => "https://www.w3.org/ns/activitystreams",
947 "actor" => announcer.ap_id,
948 "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
949 "object" => post.data["object"],
950 "to" => "https://www.w3.org/ns/activitystreams#Public",
951 "cc" => [user.ap_id],
952 "type" => "Announce"
953 }
954
955 conn =
956 conn
957 |> assign(:valid_signature, true)
958 |> put_req_header("content-type", "application/activity+json")
959 |> post("/users/#{user.nickname}/inbox", data)
960
961 assert "ok" == json_response(conn, 200)
962 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
963 %Activity{} = activity = Activity.get_by_ap_id(data["id"])
964 assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
965 end
966
967 test "it accepts messages from actors that are followed by the user", %{
968 conn: conn,
969 data: data
970 } do
971 recipient = insert(:user)
972 actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
973
974 {:ok, recipient, actor} = User.follow(recipient, actor)
975
976 object =
977 data["object"]
978 |> Map.put("attributedTo", actor.ap_id)
979
980 data =
981 data
982 |> Map.put("actor", actor.ap_id)
983 |> Map.put("object", object)
984
985 conn =
986 conn
987 |> assign(:valid_signature, true)
988 |> put_req_header("content-type", "application/activity+json")
989 |> post("/users/#{recipient.nickname}/inbox", data)
990
991 assert "ok" == json_response(conn, 200)
992 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
993 assert Activity.get_by_ap_id(data["id"])
994 end
995
996 test "it rejects reads from other users", %{conn: conn} do
997 user = insert(:user)
998 other_user = insert(:user)
999
1000 conn =
1001 conn
1002 |> assign(:user, other_user)
1003 |> put_req_header("accept", "application/activity+json")
1004 |> get("/users/#{user.nickname}/inbox")
1005
1006 assert json_response(conn, 403)
1007 end
1008
1009 test "it returns a note activity in a collection", %{conn: conn} do
1010 note_activity = insert(:direct_note_activity)
1011 note_object = Object.normalize(note_activity, fetch: false)
1012 user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
1013
1014 conn =
1015 conn
1016 |> assign(:user, user)
1017 |> put_req_header("accept", "application/activity+json")
1018 |> get("/users/#{user.nickname}/inbox?page=true")
1019
1020 assert response(conn, 200) =~ note_object.data["content"]
1021 end
1022
1023 test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
1024 user = insert(:user)
1025 data = Map.put(data, "bcc", [user.ap_id])
1026
1027 sender_host = URI.parse(data["actor"]).host
1028 Instances.set_consistently_unreachable(sender_host)
1029 refute Instances.reachable?(sender_host)
1030
1031 conn =
1032 conn
1033 |> assign(:valid_signature, true)
1034 |> put_req_header("content-type", "application/activity+json")
1035 |> post("/users/#{user.nickname}/inbox", data)
1036
1037 assert "ok" == json_response(conn, 200)
1038 assert Instances.reachable?(sender_host)
1039 end
1040
1041 @tag capture_log: true
1042 test "it removes all follower collections but actor's", %{conn: conn} do
1043 [actor, recipient] = insert_pair(:user)
1044
1045 to = [
1046 recipient.ap_id,
1047 recipient.follower_address,
1048 "https://www.w3.org/ns/activitystreams#Public"
1049 ]
1050
1051 cc = [recipient.follower_address, actor.follower_address]
1052
1053 data = %{
1054 "@context" => ["https://www.w3.org/ns/activitystreams"],
1055 "type" => "Create",
1056 "id" => Utils.generate_activity_id(),
1057 "to" => to,
1058 "cc" => cc,
1059 "actor" => actor.ap_id,
1060 "object" => %{
1061 "type" => "Note",
1062 "to" => to,
1063 "cc" => cc,
1064 "content" => "It's a note",
1065 "attributedTo" => actor.ap_id,
1066 "id" => Utils.generate_object_id()
1067 }
1068 }
1069
1070 conn
1071 |> assign(:valid_signature, true)
1072 |> put_req_header("content-type", "application/activity+json")
1073 |> post("/users/#{recipient.nickname}/inbox", data)
1074 |> json_response(200)
1075
1076 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
1077
1078 assert activity = Activity.get_by_ap_id(data["id"])
1079
1080 assert activity.id
1081 assert actor.follower_address in activity.recipients
1082 assert actor.follower_address in activity.data["cc"]
1083
1084 refute recipient.follower_address in activity.recipients
1085 refute recipient.follower_address in activity.data["cc"]
1086 refute recipient.follower_address in activity.data["to"]
1087 end
1088
1089 test "it requires authentication", %{conn: conn} do
1090 user = insert(:user)
1091 conn = put_req_header(conn, "accept", "application/activity+json")
1092
1093 ret_conn = get(conn, "/users/#{user.nickname}/inbox")
1094 assert json_response(ret_conn, 403)
1095
1096 ret_conn =
1097 conn
1098 |> assign(:user, user)
1099 |> get("/users/#{user.nickname}/inbox")
1100
1101 assert json_response(ret_conn, 200)
1102 end
1103
1104 @tag capture_log: true
1105 test "forwarded report", %{conn: conn} do
1106 admin = insert(:user, is_admin: true)
1107 actor = insert(:user, local: false)
1108 remote_domain = URI.parse(actor.ap_id).host
1109 reported_user = insert(:user)
1110
1111 note = insert(:note_activity, user: reported_user)
1112
1113 data = %{
1114 "@context" => [
1115 "https://www.w3.org/ns/activitystreams",
1116 "https://#{remote_domain}/schemas/litepub-0.1.jsonld",
1117 %{
1118 "@language" => "und"
1119 }
1120 ],
1121 "actor" => actor.ap_id,
1122 "cc" => [
1123 reported_user.ap_id
1124 ],
1125 "content" => "test",
1126 "context" => "context",
1127 "id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
1128 "nickname" => reported_user.nickname,
1129 "object" => [
1130 reported_user.ap_id,
1131 %{
1132 "actor" => %{
1133 "actor_type" => "Person",
1134 "approval_pending" => false,
1135 "avatar" => "",
1136 "confirmation_pending" => false,
1137 "deactivated" => false,
1138 "display_name" => "test user",
1139 "id" => reported_user.id,
1140 "local" => false,
1141 "nickname" => reported_user.nickname,
1142 "registration_reason" => nil,
1143 "roles" => %{
1144 "admin" => false,
1145 "moderator" => false
1146 },
1147 "tags" => [],
1148 "url" => reported_user.ap_id
1149 },
1150 "content" => "",
1151 "id" => note.data["id"],
1152 "published" => note.data["published"],
1153 "type" => "Note"
1154 }
1155 ],
1156 "published" => note.data["published"],
1157 "state" => "open",
1158 "to" => [],
1159 "type" => "Flag"
1160 }
1161
1162 conn
1163 |> assign(:valid_signature, true)
1164 |> put_req_header("content-type", "application/activity+json")
1165 |> post("/users/#{reported_user.nickname}/inbox", data)
1166 |> json_response(200)
1167
1168 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
1169
1170 assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
1171
1172 ObanHelpers.perform_all()
1173
1174 Swoosh.TestAssertions.assert_email_sent(
1175 to: {admin.name, admin.email},
1176 html_body: ~r/Reported Account:/i
1177 )
1178 end
1179
1180 @tag capture_log: true
1181 test "forwarded report from mastodon", %{conn: conn} do
1182 admin = insert(:user, is_admin: true)
1183 actor = insert(:user, local: false)
1184 remote_domain = URI.parse(actor.ap_id).host
1185 remote_actor = "https://#{remote_domain}/actor"
1186 [reported_user, another] = insert_list(2, :user)
1187
1188 note = insert(:note_activity, user: reported_user)
1189
1190 Pleroma.Web.CommonAPI.favorite(another, note.id)
1191
1192 mock_json_body =
1193 "test/fixtures/mastodon/application_actor.json"
1194 |> File.read!()
1195 |> String.replace("{{DOMAIN}}", remote_domain)
1196
1197 Tesla.Mock.mock(fn %{url: ^remote_actor} ->
1198 %Tesla.Env{
1199 status: 200,
1200 body: mock_json_body,
1201 headers: [{"content-type", "application/activity+json"}]
1202 }
1203 end)
1204
1205 data = %{
1206 "@context" => "https://www.w3.org/ns/activitystreams",
1207 "actor" => remote_actor,
1208 "content" => "test report",
1209 "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
1210 "object" => [
1211 reported_user.ap_id,
1212 note.data["object"]
1213 ],
1214 "type" => "Flag"
1215 }
1216
1217 conn
1218 |> assign(:valid_signature, true)
1219 |> put_req_header("content-type", "application/activity+json")
1220 |> post("/users/#{reported_user.nickname}/inbox", data)
1221 |> json_response(200)
1222
1223 ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
1224
1225 flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
1226 reported_user_ap_id = reported_user.ap_id
1227
1228 [^reported_user_ap_id, flag_data] = flag_activity.data["object"]
1229
1230 Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
1231 ObanHelpers.perform_all()
1232
1233 Swoosh.TestAssertions.assert_email_sent(
1234 to: {admin.name, admin.email},
1235 html_body: ~r/#{note.data["object"]}/i
1236 )
1237 end
1238 end
1239
1240 describe "GET /users/:nickname/outbox" do
1241 test "it paginates correctly", %{conn: conn} do
1242 user = insert(:user)
1243 conn = assign(conn, :user, user)
1244 outbox_endpoint = user.ap_id <> "/outbox"
1245
1246 _posts =
1247 for i <- 0..25 do
1248 {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
1249 activity
1250 end
1251
1252 result =
1253 conn
1254 |> put_req_header("accept", "application/activity+json")
1255 |> get(outbox_endpoint <> "?page=true")
1256 |> json_response(200)
1257
1258 result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
1259 assert length(result["orderedItems"]) == 20
1260 assert length(result_ids) == 20
1261 assert result["next"]
1262 assert String.starts_with?(result["next"], outbox_endpoint)
1263
1264 result_next =
1265 conn
1266 |> put_req_header("accept", "application/activity+json")
1267 |> get(result["next"])
1268 |> json_response(200)
1269
1270 result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
1271 assert length(result_next["orderedItems"]) == 6
1272 assert length(result_next_ids) == 6
1273 refute Enum.find(result_next_ids, fn x -> x in result_ids end)
1274 refute Enum.find(result_ids, fn x -> x in result_next_ids end)
1275 assert String.starts_with?(result["id"], outbox_endpoint)
1276
1277 result_next_again =
1278 conn
1279 |> put_req_header("accept", "application/activity+json")
1280 |> get(result_next["id"])
1281 |> json_response(200)
1282
1283 assert result_next == result_next_again
1284 end
1285
1286 test "it returns 200 even if there're no activities", %{conn: conn} do
1287 user = insert(:user)
1288 outbox_endpoint = user.ap_id <> "/outbox"
1289
1290 conn =
1291 conn
1292 |> assign(:user, user)
1293 |> put_req_header("accept", "application/activity+json")
1294 |> get(outbox_endpoint)
1295
1296 result = json_response(conn, 200)
1297 assert outbox_endpoint == result["id"]
1298 end
1299
1300 test "it returns a note activity in a collection", %{conn: conn} do
1301 note_activity = insert(:note_activity)
1302 note_object = Object.normalize(note_activity, fetch: false)
1303 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1304
1305 conn =
1306 conn
1307 |> assign(:user, user)
1308 |> put_req_header("accept", "application/activity+json")
1309 |> get("/users/#{user.nickname}/outbox?page=true")
1310
1311 assert response(conn, 200) =~ note_object.data["content"]
1312 end
1313
1314 test "it returns an announce activity in a collection", %{conn: conn} do
1315 announce_activity = insert(:announce_activity)
1316 user = User.get_cached_by_ap_id(announce_activity.data["actor"])
1317
1318 conn =
1319 conn
1320 |> assign(:user, user)
1321 |> put_req_header("accept", "application/activity+json")
1322 |> get("/users/#{user.nickname}/outbox?page=true")
1323
1324 assert response(conn, 200) =~ announce_activity.data["object"]
1325 end
1326
1327 test "It returns poll Answers when authenticated", %{conn: conn} do
1328 poller = insert(:user)
1329 voter = insert(:user)
1330
1331 {:ok, activity} =
1332 CommonAPI.post(poller, %{
1333 status: "suya...",
1334 poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
1335 })
1336
1337 assert question = Object.normalize(activity, fetch: false)
1338
1339 {:ok, [activity], _object} = CommonAPI.vote(voter, question, [1])
1340
1341 assert outbox_get =
1342 conn
1343 |> assign(:user, voter)
1344 |> put_req_header("accept", "application/activity+json")
1345 |> get(voter.ap_id <> "/outbox?page=true")
1346 |> json_response(200)
1347
1348 assert [answer_outbox] = outbox_get["orderedItems"]
1349 assert answer_outbox["id"] == activity.data["id"]
1350 end
1351 end
1352
1353 describe "POST /users/:nickname/outbox (C2S)" do
1354 setup do: clear_config([:instance, :limit])
1355
1356 setup do
1357 [
1358 activity: %{
1359 "@context" => "https://www.w3.org/ns/activitystreams",
1360 "type" => "Create",
1361 "object" => %{
1362 "type" => "Note",
1363 "content" => "AP C2S test",
1364 "to" => "https://www.w3.org/ns/activitystreams#Public",
1365 "cc" => []
1366 }
1367 }
1368 ]
1369 end
1370
1371 test "it rejects posts from other users / unauthenticated users", %{
1372 conn: conn,
1373 activity: activity
1374 } do
1375 user = insert(:user)
1376 other_user = insert(:user)
1377 conn = put_req_header(conn, "content-type", "application/activity+json")
1378
1379 conn
1380 |> post("/users/#{user.nickname}/outbox", activity)
1381 |> json_response(403)
1382
1383 conn
1384 |> assign(:user, other_user)
1385 |> post("/users/#{user.nickname}/outbox", activity)
1386 |> json_response(403)
1387 end
1388
1389 test "it inserts an incoming create activity into the database", %{
1390 conn: conn,
1391 activity: activity
1392 } do
1393 user = insert(:user)
1394
1395 result =
1396 conn
1397 |> assign(:user, user)
1398 |> put_req_header("content-type", "application/activity+json")
1399 |> post("/users/#{user.nickname}/outbox", activity)
1400 |> json_response(201)
1401
1402 assert Activity.get_by_ap_id(result["id"])
1403 assert result["object"]
1404 assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
1405 assert object["content"] == activity["object"]["content"]
1406 end
1407
1408 test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
1409 user = insert(:user)
1410
1411 activity =
1412 activity
1413 |> put_in(["object", "type"], "Benis")
1414
1415 _result =
1416 conn
1417 |> assign(:user, user)
1418 |> put_req_header("content-type", "application/activity+json")
1419 |> post("/users/#{user.nickname}/outbox", activity)
1420 |> json_response(400)
1421 end
1422
1423 test "it inserts an incoming sensitive activity into the database", %{
1424 conn: conn,
1425 activity: activity
1426 } do
1427 user = insert(:user)
1428 conn = assign(conn, :user, user)
1429 object = Map.put(activity["object"], "sensitive", true)
1430 activity = Map.put(activity, "object", object)
1431
1432 response =
1433 conn
1434 |> put_req_header("content-type", "application/activity+json")
1435 |> post("/users/#{user.nickname}/outbox", activity)
1436 |> json_response(201)
1437
1438 assert Activity.get_by_ap_id(response["id"])
1439 assert response["object"]
1440 assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false)
1441 assert response_object["sensitive"] == true
1442 assert response_object["content"] == activity["object"]["content"]
1443
1444 representation =
1445 conn
1446 |> put_req_header("accept", "application/activity+json")
1447 |> get(response["id"])
1448 |> json_response(200)
1449
1450 assert representation["object"]["sensitive"] == true
1451 end
1452
1453 test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
1454 user = insert(:user)
1455 activity = Map.put(activity, "type", "BadType")
1456
1457 conn =
1458 conn
1459 |> assign(:user, user)
1460 |> put_req_header("content-type", "application/activity+json")
1461 |> post("/users/#{user.nickname}/outbox", activity)
1462
1463 assert json_response(conn, 400)
1464 end
1465
1466 test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
1467 note_activity = insert(:note_activity)
1468 note_object = Object.normalize(note_activity, fetch: false)
1469 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1470
1471 data = %{
1472 "type" => "Delete",
1473 "object" => %{
1474 "id" => note_object.data["id"]
1475 }
1476 }
1477
1478 result =
1479 conn
1480 |> assign(:user, user)
1481 |> put_req_header("content-type", "application/activity+json")
1482 |> post("/users/#{user.nickname}/outbox", data)
1483 |> json_response(201)
1484
1485 assert Activity.get_by_ap_id(result["id"])
1486
1487 assert object = Object.get_by_ap_id(note_object.data["id"])
1488 assert object.data["type"] == "Tombstone"
1489 end
1490
1491 test "it rejects delete activity of object from other actor", %{conn: conn} do
1492 note_activity = insert(:note_activity)
1493 note_object = Object.normalize(note_activity, fetch: false)
1494 user = insert(:user)
1495
1496 data = %{
1497 type: "Delete",
1498 object: %{
1499 id: note_object.data["id"]
1500 }
1501 }
1502
1503 conn =
1504 conn
1505 |> assign(:user, user)
1506 |> put_req_header("content-type", "application/activity+json")
1507 |> post("/users/#{user.nickname}/outbox", data)
1508
1509 assert json_response(conn, 403)
1510 end
1511
1512 test "it increases like count when receiving a like action", %{conn: conn} do
1513 note_activity = insert(:note_activity)
1514 note_object = Object.normalize(note_activity, fetch: false)
1515 user = User.get_cached_by_ap_id(note_activity.data["actor"])
1516
1517 data = %{
1518 type: "Like",
1519 object: %{
1520 id: note_object.data["id"]
1521 }
1522 }
1523
1524 conn =
1525 conn
1526 |> assign(:user, user)
1527 |> put_req_header("content-type", "application/activity+json")
1528 |> post("/users/#{user.nickname}/outbox", data)
1529
1530 result = json_response(conn, 201)
1531 assert Activity.get_by_ap_id(result["id"])
1532
1533 assert object = Object.get_by_ap_id(note_object.data["id"])
1534 assert object.data["like_count"] == 1
1535 end
1536
1537 test "it doesn't spreads faulty attributedTo or actor fields", %{
1538 conn: conn,
1539 activity: activity
1540 } do
1541 reimu = insert(:user, nickname: "reimu")
1542 cirno = insert(:user, nickname: "cirno")
1543
1544 assert reimu.ap_id
1545 assert cirno.ap_id
1546
1547 activity =
1548 activity
1549 |> put_in(["object", "actor"], reimu.ap_id)
1550 |> put_in(["object", "attributedTo"], reimu.ap_id)
1551 |> put_in(["actor"], reimu.ap_id)
1552 |> put_in(["attributedTo"], reimu.ap_id)
1553
1554 _reimu_outbox =
1555 conn
1556 |> assign(:user, cirno)
1557 |> put_req_header("content-type", "application/activity+json")
1558 |> post("/users/#{reimu.nickname}/outbox", activity)
1559 |> json_response(403)
1560
1561 cirno_outbox =
1562 conn
1563 |> assign(:user, cirno)
1564 |> put_req_header("content-type", "application/activity+json")
1565 |> post("/users/#{cirno.nickname}/outbox", activity)
1566 |> json_response(201)
1567
1568 assert cirno_outbox["attributedTo"] == nil
1569 assert cirno_outbox["actor"] == cirno.ap_id
1570
1571 assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false)
1572 assert cirno_object.data["actor"] == cirno.ap_id
1573 assert cirno_object.data["attributedTo"] == cirno.ap_id
1574 end
1575
1576 test "Character limitation", %{conn: conn, activity: activity} do
1577 clear_config([:instance, :limit], 5)
1578 user = insert(:user)
1579
1580 result =
1581 conn
1582 |> assign(:user, user)
1583 |> put_req_header("content-type", "application/activity+json")
1584 |> post("/users/#{user.nickname}/outbox", activity)
1585 |> json_response(400)
1586
1587 assert result == "Character limit (5 characters) exceeded, contains 11 characters"
1588 end
1589 end
1590
1591 describe "/relay/followers" do
1592 test "it returns relay followers", %{conn: conn} do
1593 relay_actor = Relay.get_actor()
1594 user = insert(:user)
1595 User.follow(user, relay_actor)
1596
1597 result =
1598 conn
1599 |> get("/relay/followers")
1600 |> json_response(200)
1601
1602 assert result["first"]["orderedItems"] == [user.ap_id]
1603 end
1604
1605 test "on non-federating instance, it returns 404", %{conn: conn} do
1606 clear_config([:instance, :federating], false)
1607 user = insert(:user)
1608
1609 conn
1610 |> assign(:user, user)
1611 |> get("/relay/followers")
1612 |> json_response(404)
1613 end
1614 end
1615
1616 describe "/relay/following" do
1617 test "it returns relay following", %{conn: conn} do
1618 result =
1619 conn
1620 |> get("/relay/following")
1621 |> json_response(200)
1622
1623 assert result["first"]["orderedItems"] == []
1624 end
1625
1626 test "on non-federating instance, it returns 404", %{conn: conn} do
1627 clear_config([:instance, :federating], false)
1628 user = insert(:user)
1629
1630 conn
1631 |> assign(:user, user)
1632 |> get("/relay/following")
1633 |> json_response(404)
1634 end
1635 end
1636
1637 describe "/users/:nickname/followers" do
1638 test "it returns the followers in a collection", %{conn: conn} do
1639 user = insert(:user)
1640 user_two = insert(:user)
1641 User.follow(user, user_two)
1642
1643 result =
1644 conn
1645 |> assign(:user, user_two)
1646 |> get("/users/#{user_two.nickname}/followers")
1647 |> json_response(200)
1648
1649 assert result["first"]["orderedItems"] == [user.ap_id]
1650 end
1651
1652 test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
1653 user = insert(:user)
1654 user_two = insert(:user, hide_followers: true)
1655 User.follow(user, user_two)
1656
1657 result =
1658 conn
1659 |> assign(:user, user)
1660 |> get("/users/#{user_two.nickname}/followers")
1661 |> json_response(200)
1662
1663 assert is_binary(result["first"])
1664 end
1665
1666 test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
1667 %{conn: conn} do
1668 user = insert(:user)
1669 other_user = insert(:user, hide_followers: true)
1670
1671 result =
1672 conn
1673 |> assign(:user, user)
1674 |> get("/users/#{other_user.nickname}/followers?page=1")
1675
1676 assert result.status == 403
1677 assert result.resp_body == ""
1678 end
1679
1680 test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
1681 %{conn: conn} do
1682 user = insert(:user, hide_followers: true)
1683 other_user = insert(:user)
1684 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
1685
1686 result =
1687 conn
1688 |> assign(:user, user)
1689 |> get("/users/#{user.nickname}/followers?page=1")
1690 |> json_response(200)
1691
1692 assert result["totalItems"] == 1
1693 assert result["orderedItems"] == [other_user.ap_id]
1694 end
1695
1696 test "it works for more than 10 users", %{conn: conn} do
1697 user = insert(:user)
1698
1699 Enum.each(1..15, fn _ ->
1700 other_user = insert(:user)
1701 User.follow(other_user, user)
1702 end)
1703
1704 result =
1705 conn
1706 |> assign(:user, user)
1707 |> get("/users/#{user.nickname}/followers")
1708 |> json_response(200)
1709
1710 assert length(result["first"]["orderedItems"]) == 10
1711 assert result["first"]["totalItems"] == 15
1712 assert result["totalItems"] == 15
1713
1714 result =
1715 conn
1716 |> assign(:user, user)
1717 |> get("/users/#{user.nickname}/followers?page=2")
1718 |> json_response(200)
1719
1720 assert length(result["orderedItems"]) == 5
1721 assert result["totalItems"] == 15
1722 end
1723
1724 test "does not require authentication", %{conn: conn} do
1725 user = insert(:user)
1726
1727 conn
1728 |> get("/users/#{user.nickname}/followers")
1729 |> json_response(200)
1730 end
1731 end
1732
1733 describe "/users/:nickname/following" do
1734 test "it returns the following in a collection", %{conn: conn} do
1735 user = insert(:user)
1736 user_two = insert(:user)
1737 User.follow(user, user_two)
1738
1739 result =
1740 conn
1741 |> assign(:user, user)
1742 |> get("/users/#{user.nickname}/following")
1743 |> json_response(200)
1744
1745 assert result["first"]["orderedItems"] == [user_two.ap_id]
1746 end
1747
1748 test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
1749 user = insert(:user)
1750 user_two = insert(:user, hide_follows: true)
1751 User.follow(user, user_two)
1752
1753 result =
1754 conn
1755 |> assign(:user, user)
1756 |> get("/users/#{user_two.nickname}/following")
1757 |> json_response(200)
1758
1759 assert is_binary(result["first"])
1760 end
1761
1762 test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
1763 %{conn: conn} do
1764 user = insert(:user)
1765 user_two = insert(:user, hide_follows: true)
1766
1767 result =
1768 conn
1769 |> assign(:user, user)
1770 |> get("/users/#{user_two.nickname}/following?page=1")
1771
1772 assert result.status == 403
1773 assert result.resp_body == ""
1774 end
1775
1776 test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
1777 %{conn: conn} do
1778 user = insert(:user, hide_follows: true)
1779 other_user = insert(:user)
1780 {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
1781
1782 result =
1783 conn
1784 |> assign(:user, user)
1785 |> get("/users/#{user.nickname}/following?page=1")
1786 |> json_response(200)
1787
1788 assert result["totalItems"] == 1
1789 assert result["orderedItems"] == [other_user.ap_id]
1790 end
1791
1792 test "it works for more than 10 users", %{conn: conn} do
1793 user = insert(:user)
1794
1795 Enum.each(1..15, fn _ ->
1796 user = User.get_cached_by_id(user.id)
1797 other_user = insert(:user)
1798 User.follow(user, other_user)
1799 end)
1800
1801 result =
1802 conn
1803 |> assign(:user, user)
1804 |> get("/users/#{user.nickname}/following")
1805 |> json_response(200)
1806
1807 assert length(result["first"]["orderedItems"]) == 10
1808 assert result["first"]["totalItems"] == 15
1809 assert result["totalItems"] == 15
1810
1811 result =
1812 conn
1813 |> assign(:user, user)
1814 |> get("/users/#{user.nickname}/following?page=2")
1815 |> json_response(200)
1816
1817 assert length(result["orderedItems"]) == 5
1818 assert result["totalItems"] == 15
1819 end
1820
1821 test "does not require authentication", %{conn: conn} do
1822 user = insert(:user)
1823
1824 conn
1825 |> get("/users/#{user.nickname}/following")
1826 |> json_response(200)
1827 end
1828 end
1829
1830 describe "delivery tracking" do
1831 test "it tracks a signed object fetch", %{conn: conn} do
1832 user = insert(:user, local: false)
1833 activity = insert(:note_activity)
1834 object = Object.normalize(activity, fetch: false)
1835
1836 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1837
1838 conn
1839 |> put_req_header("accept", "application/activity+json")
1840 |> assign(:user, user)
1841 |> get(object_path)
1842 |> json_response(200)
1843
1844 assert Delivery.get(object.id, user.id)
1845 end
1846
1847 test "it tracks a signed activity fetch", %{conn: conn} do
1848 user = insert(:user, local: false)
1849 activity = insert(:note_activity)
1850 object = Object.normalize(activity, fetch: false)
1851
1852 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1853
1854 conn
1855 |> put_req_header("accept", "application/activity+json")
1856 |> assign(:user, user)
1857 |> get(activity_path)
1858 |> json_response(200)
1859
1860 assert Delivery.get(object.id, user.id)
1861 end
1862
1863 test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
1864 user = insert(:user, local: false)
1865 other_user = insert(:user, local: false)
1866 activity = insert(:note_activity)
1867 object = Object.normalize(activity, fetch: false)
1868
1869 object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
1870
1871 conn
1872 |> put_req_header("accept", "application/activity+json")
1873 |> assign(:user, user)
1874 |> get(object_path)
1875 |> json_response(200)
1876
1877 build_conn()
1878 |> put_req_header("accept", "application/activity+json")
1879 |> assign(:user, other_user)
1880 |> get(object_path)
1881 |> json_response(200)
1882
1883 assert Delivery.get(object.id, user.id)
1884 assert Delivery.get(object.id, other_user.id)
1885 end
1886
1887 test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
1888 user = insert(:user, local: false)
1889 other_user = insert(:user, local: false)
1890 activity = insert(:note_activity)
1891 object = Object.normalize(activity, fetch: false)
1892
1893 activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
1894
1895 conn
1896 |> put_req_header("accept", "application/activity+json")
1897 |> assign(:user, user)
1898 |> get(activity_path)
1899 |> json_response(200)
1900
1901 build_conn()
1902 |> put_req_header("accept", "application/activity+json")
1903 |> assign(:user, other_user)
1904 |> get(activity_path)
1905 |> json_response(200)
1906
1907 assert Delivery.get(object.id, user.id)
1908 assert Delivery.get(object.id, other_user.id)
1909 end
1910 end
1911
1912 describe "Additional ActivityPub C2S endpoints" do
1913 test "GET /api/ap/whoami", %{conn: conn} do
1914 user = insert(:user)
1915
1916 conn =
1917 conn
1918 |> assign(:user, user)
1919 |> get("/api/ap/whoami")
1920
1921 user = User.get_cached_by_id(user.id)
1922
1923 assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
1924
1925 conn
1926 |> get("/api/ap/whoami")
1927 |> json_response(403)
1928 end
1929
1930 setup do: clear_config([:media_proxy])
1931 setup do: clear_config([Pleroma.Upload])
1932
1933 test "POST /api/ap/upload_media", %{conn: conn} do
1934 user = insert(:user)
1935
1936 desc = "Description of the image"
1937
1938 image = %Plug.Upload{
1939 content_type: "image/jpeg",
1940 path: Path.absname("test/fixtures/image.jpg"),
1941 filename: "an_image.jpg"
1942 }
1943
1944 object =
1945 conn
1946 |> assign(:user, user)
1947 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1948 |> json_response(:created)
1949
1950 assert object["name"] == desc
1951 assert object["type"] == "Document"
1952 assert object["actor"] == user.ap_id
1953 assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
1954 assert is_binary(object_href)
1955 assert object_mediatype == "image/jpeg"
1956 assert String.ends_with?(object_href, ".jpg")
1957
1958 activity_request = %{
1959 "@context" => "https://www.w3.org/ns/activitystreams",
1960 "type" => "Create",
1961 "object" => %{
1962 "type" => "Note",
1963 "content" => "AP C2S test, attachment",
1964 "attachment" => [object],
1965 "to" => "https://www.w3.org/ns/activitystreams#Public",
1966 "cc" => []
1967 }
1968 }
1969
1970 activity_response =
1971 conn
1972 |> assign(:user, user)
1973 |> post("/users/#{user.nickname}/outbox", activity_request)
1974 |> json_response(:created)
1975
1976 assert activity_response["id"]
1977 assert activity_response["object"]
1978 assert activity_response["actor"] == user.ap_id
1979
1980 assert %Object{data: %{"attachment" => [attachment]}} =
1981 Object.normalize(activity_response["object"], fetch: false)
1982
1983 assert attachment["type"] == "Document"
1984 assert attachment["name"] == desc
1985
1986 assert [
1987 %{
1988 "href" => ^object_href,
1989 "type" => "Link",
1990 "mediaType" => ^object_mediatype
1991 }
1992 ] = attachment["url"]
1993
1994 # Fails if unauthenticated
1995 conn
1996 |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
1997 |> json_response(403)
1998 end
1999 end
2000
2001 test "pinned collection", %{conn: conn} do
2002 clear_config([:instance, :max_pinned_statuses], 2)
2003 user = insert(:user)
2004 objects = insert_list(2, :note, user: user)
2005
2006 Enum.reduce(objects, user, fn %{data: %{"id" => object_id}}, user ->
2007 {:ok, updated} = User.add_pinned_object_id(user, object_id)
2008 updated
2009 end)
2010
2011 %{nickname: nickname, featured_address: featured_address, pinned_objects: pinned_objects} =
2012 refresh_record(user)
2013
2014 %{"id" => ^featured_address, "orderedItems" => items, "totalItems" => 2} =
2015 conn
2016 |> get("/users/#{nickname}/collections/featured")
2017 |> json_response(200)
2018
2019 object_ids = Enum.map(items, & &1["id"])
2020
2021 assert Enum.all?(pinned_objects, fn {obj_id, _} ->
2022 obj_id in object_ids
2023 end)
2024 end
2025 end