Pleroma.Constants.as_local_public/0 --> Pleroma.Web.ActivityPub.Utils.as_local_public/0
[akkoma] / test / pleroma / web / mastodon_api / controllers / status_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.MastodonAPI.StatusControllerTest do
6 use Pleroma.Web.ConnCase
7 use Oban.Testing, repo: Pleroma.Repo
8
9 alias Pleroma.Activity
10 alias Pleroma.Conversation.Participation
11 alias Pleroma.Object
12 alias Pleroma.Repo
13 alias Pleroma.ScheduledActivity
14 alias Pleroma.Tests.ObanHelpers
15 alias Pleroma.User
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.CommonAPI
19
20 import Pleroma.Factory
21
22 setup do: clear_config([:instance, :federating])
23 setup do: clear_config([:instance, :allow_relay])
24 setup do: clear_config([:rich_media, :enabled])
25 setup do: clear_config([:mrf, :policies])
26 setup do: clear_config([:mrf_keyword, :reject])
27
28 describe "posting statuses" do
29 setup do: oauth_access(["write:statuses"])
30
31 test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
32 clear_config([:instance, :federating], true)
33 Config.get([:instance, :allow_relay], true)
34
35 response =
36 conn
37 |> put_req_header("content-type", "application/json")
38 |> post("api/v1/statuses", %{
39 "content_type" => "text/plain",
40 "source" => "Pleroma FE",
41 "status" => "Hello world",
42 "visibility" => "public"
43 })
44 |> json_response_and_validate_schema(200)
45
46 assert response["reblogs_count"] == 0
47 ObanHelpers.perform_all()
48
49 response =
50 conn
51 |> get("api/v1/statuses/#{response["id"]}", %{})
52 |> json_response_and_validate_schema(200)
53
54 assert response["reblogs_count"] == 0
55 end
56
57 test "posting a status", %{conn: conn} do
58 idempotency_key = "Pikachu rocks!"
59
60 conn_one =
61 conn
62 |> put_req_header("content-type", "application/json")
63 |> put_req_header("idempotency-key", idempotency_key)
64 |> post("/api/v1/statuses", %{
65 "status" => "cofe",
66 "spoiler_text" => "2hu",
67 "sensitive" => "0"
68 })
69
70 assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
71 json_response_and_validate_schema(conn_one, 200)
72
73 assert Activity.get_by_id(id)
74
75 conn_two =
76 conn
77 |> put_req_header("content-type", "application/json")
78 |> put_req_header("idempotency-key", idempotency_key)
79 |> post("/api/v1/statuses", %{
80 "status" => "cofe",
81 "spoiler_text" => "2hu",
82 "sensitive" => 0
83 })
84
85 assert %{"id" => second_id} = json_response(conn_two, 200)
86 assert id == second_id
87
88 conn_three =
89 conn
90 |> put_req_header("content-type", "application/json")
91 |> post("/api/v1/statuses", %{
92 "status" => "cofe",
93 "spoiler_text" => "2hu",
94 "sensitive" => "False"
95 })
96
97 assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
98 refute id == third_id
99
100 # An activity that will expire:
101 # 2 hours
102 expires_in = 2 * 60 * 60
103
104 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
105
106 conn_four =
107 conn
108 |> put_req_header("content-type", "application/json")
109 |> post("api/v1/statuses", %{
110 "status" => "oolong",
111 "expires_in" => expires_in
112 })
113
114 assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
115
116 assert Activity.get_by_id(fourth_id)
117
118 assert_enqueued(
119 worker: Pleroma.Workers.PurgeExpiredActivity,
120 args: %{activity_id: fourth_id},
121 scheduled_at: expires_at
122 )
123 end
124
125 test "it fails to create a status if `expires_in` is less or equal than an hour", %{
126 conn: conn
127 } do
128 # 1 minute
129 expires_in = 1 * 60
130
131 assert %{"error" => "Expiry date is too soon"} =
132 conn
133 |> put_req_header("content-type", "application/json")
134 |> post("api/v1/statuses", %{
135 "status" => "oolong",
136 "expires_in" => expires_in
137 })
138 |> json_response_and_validate_schema(422)
139
140 # 5 minutes
141 expires_in = 5 * 60
142
143 assert %{"error" => "Expiry date is too soon"} =
144 conn
145 |> put_req_header("content-type", "application/json")
146 |> post("api/v1/statuses", %{
147 "status" => "oolong",
148 "expires_in" => expires_in
149 })
150 |> json_response_and_validate_schema(422)
151 end
152
153 test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
154 clear_config([:mrf_keyword, :reject], ["GNO"])
155 clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
156
157 assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
158 conn
159 |> put_req_header("content-type", "application/json")
160 |> post("api/v1/statuses", %{"status" => "GNO/Linux"})
161 |> json_response_and_validate_schema(422)
162 end
163
164 test "posting an undefined status with an attachment", %{user: user, conn: conn} do
165 file = %Plug.Upload{
166 content_type: "image/jpeg",
167 path: Path.absname("test/fixtures/image.jpg"),
168 filename: "an_image.jpg"
169 }
170
171 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
172
173 conn =
174 conn
175 |> put_req_header("content-type", "application/json")
176 |> post("/api/v1/statuses", %{
177 "media_ids" => [to_string(upload.id)]
178 })
179
180 assert json_response_and_validate_schema(conn, 200)
181 end
182
183 test "replying to a status", %{user: user, conn: conn} do
184 {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
185
186 conn =
187 conn
188 |> put_req_header("content-type", "application/json")
189 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
190
191 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
192
193 activity = Activity.get_by_id(id)
194
195 assert activity.data["context"] == replied_to.data["context"]
196 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
197 end
198
199 test "replying to a direct message with visibility other than direct", %{
200 user: user,
201 conn: conn
202 } do
203 {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
204
205 Enum.each(["public", "private", "unlisted"], fn visibility ->
206 conn =
207 conn
208 |> put_req_header("content-type", "application/json")
209 |> post("/api/v1/statuses", %{
210 "status" => "@#{user.nickname} hey",
211 "in_reply_to_id" => replied_to.id,
212 "visibility" => visibility
213 })
214
215 assert json_response_and_validate_schema(conn, 422) == %{
216 "error" => "The message visibility must be direct"
217 }
218 end)
219 end
220
221 test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
222 conn =
223 conn
224 |> put_req_header("content-type", "application/json")
225 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
226
227 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
228 assert Activity.get_by_id(id)
229 end
230
231 test "posting a sensitive status", %{conn: conn} do
232 conn =
233 conn
234 |> put_req_header("content-type", "application/json")
235 |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
236
237 assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
238 json_response_and_validate_schema(conn, 200)
239
240 assert Activity.get_by_id(id)
241 end
242
243 test "posting a fake status", %{conn: conn} do
244 real_conn =
245 conn
246 |> put_req_header("content-type", "application/json")
247 |> post("/api/v1/statuses", %{
248 "status" =>
249 "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
250 })
251
252 real_status = json_response_and_validate_schema(real_conn, 200)
253
254 assert real_status
255 assert Object.get_by_ap_id(real_status["uri"])
256
257 real_status =
258 real_status
259 |> Map.put("id", nil)
260 |> Map.put("url", nil)
261 |> Map.put("uri", nil)
262 |> Map.put("created_at", nil)
263 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
264
265 fake_conn =
266 conn
267 |> assign(:user, refresh_record(conn.assigns.user))
268 |> put_req_header("content-type", "application/json")
269 |> post("/api/v1/statuses", %{
270 "status" =>
271 "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
272 "preview" => true
273 })
274
275 fake_status = json_response_and_validate_schema(fake_conn, 200)
276
277 assert fake_status
278 refute Object.get_by_ap_id(fake_status["uri"])
279
280 fake_status =
281 fake_status
282 |> Map.put("id", nil)
283 |> Map.put("url", nil)
284 |> Map.put("uri", nil)
285 |> Map.put("created_at", nil)
286 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
287
288 assert real_status == fake_status
289 end
290
291 test "fake statuses' preview card is not cached", %{conn: conn} do
292 clear_config([:rich_media, :enabled], true)
293
294 Tesla.Mock.mock(fn
295 %{
296 method: :get,
297 url: "https://example.com/twitter-card"
298 } ->
299 %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
300
301 env ->
302 apply(HttpRequestMock, :request, [env])
303 end)
304
305 conn1 =
306 conn
307 |> put_req_header("content-type", "application/json")
308 |> post("/api/v1/statuses", %{
309 "status" => "https://example.com/ogp",
310 "preview" => true
311 })
312
313 conn2 =
314 conn
315 |> put_req_header("content-type", "application/json")
316 |> post("/api/v1/statuses", %{
317 "status" => "https://example.com/twitter-card",
318 "preview" => true
319 })
320
321 assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200)
322
323 assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} =
324 json_response_and_validate_schema(conn2, 200)
325 end
326
327 test "posting a status with OGP link preview", %{conn: conn} do
328 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
329 clear_config([:rich_media, :enabled], true)
330
331 conn =
332 conn
333 |> put_req_header("content-type", "application/json")
334 |> post("/api/v1/statuses", %{
335 "status" => "https://example.com/ogp"
336 })
337
338 assert %{"id" => id, "card" => %{"title" => "The Rock"}} =
339 json_response_and_validate_schema(conn, 200)
340
341 assert Activity.get_by_id(id)
342 end
343
344 test "posting a direct status", %{conn: conn} do
345 user2 = insert(:user)
346 content = "direct cofe @#{user2.nickname}"
347
348 conn =
349 conn
350 |> put_req_header("content-type", "application/json")
351 |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
352
353 assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
354 assert response["visibility"] == "direct"
355 assert response["pleroma"]["direct_conversation_id"]
356 assert activity = Activity.get_by_id(id)
357 assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
358 assert activity.data["to"] == [user2.ap_id]
359 assert activity.data["cc"] == []
360 end
361
362 @tag :skip
363 test "discloses application metadata when enabled" do
364 user = insert(:user, disclose_client: true)
365 %{user: _user, token: token, conn: conn} = oauth_access(["write:statuses"], user: user)
366
367 %Pleroma.Web.OAuth.Token{
368 app: %Pleroma.Web.OAuth.App{
369 client_name: app_name,
370 website: app_website
371 }
372 } = token
373
374 result =
375 conn
376 |> put_req_header("content-type", "application/json")
377 |> post("/api/v1/statuses", %{
378 "status" => "cofe is my copilot"
379 })
380
381 assert %{
382 "content" => "cofe is my copilot",
383 "application" => %{
384 "name" => ^app_name,
385 "website" => ^app_website
386 }
387 } = json_response_and_validate_schema(result, 200)
388 end
389
390 test "hides application metadata when disabled" do
391 user = insert(:user, disclose_client: false)
392 %{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
393
394 result =
395 conn
396 |> put_req_header("content-type", "application/json")
397 |> post("/api/v1/statuses", %{
398 "status" => "club mate is my wingman"
399 })
400
401 assert %{
402 "content" => "club mate is my wingman",
403 "application" => nil
404 } = json_response_and_validate_schema(result, 200)
405 end
406 end
407
408 describe "posting scheduled statuses" do
409 setup do: oauth_access(["write:statuses"])
410
411 test "creates a scheduled activity", %{conn: conn} do
412 scheduled_at =
413 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
414 |> NaiveDateTime.to_iso8601()
415 |> Kernel.<>("Z")
416
417 conn =
418 conn
419 |> put_req_header("content-type", "application/json")
420 |> post("/api/v1/statuses", %{
421 "status" => "scheduled",
422 "scheduled_at" => scheduled_at
423 })
424
425 assert %{"scheduled_at" => expected_scheduled_at} =
426 json_response_and_validate_schema(conn, 200)
427
428 assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
429 assert [] == Repo.all(Activity)
430 end
431
432 test "with expiration" do
433 %{conn: conn} = oauth_access(["write:statuses", "read:statuses"])
434
435 scheduled_at =
436 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
437 |> NaiveDateTime.to_iso8601()
438 |> Kernel.<>("Z")
439
440 assert %{"id" => status_id, "params" => %{"expires_in" => 300}} =
441 conn
442 |> put_req_header("content-type", "application/json")
443 |> post("/api/v1/statuses", %{
444 "status" => "scheduled",
445 "scheduled_at" => scheduled_at,
446 "expires_in" => 300
447 })
448 |> json_response_and_validate_schema(200)
449
450 assert %{"id" => ^status_id, "params" => %{"expires_in" => 300}} =
451 conn
452 |> put_req_header("content-type", "application/json")
453 |> get("/api/v1/scheduled_statuses/#{status_id}")
454 |> json_response_and_validate_schema(200)
455 end
456
457 test "ignores nil values", %{conn: conn} do
458 conn =
459 conn
460 |> put_req_header("content-type", "application/json")
461 |> post("/api/v1/statuses", %{
462 "status" => "not scheduled",
463 "scheduled_at" => nil
464 })
465
466 assert result = json_response_and_validate_schema(conn, 200)
467 assert Activity.get_by_id(result["id"])
468 end
469
470 test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
471 scheduled_at =
472 NaiveDateTime.utc_now()
473 |> NaiveDateTime.add(:timer.minutes(120), :millisecond)
474 |> NaiveDateTime.to_iso8601()
475 |> Kernel.<>("Z")
476
477 file = %Plug.Upload{
478 content_type: "image/jpeg",
479 path: Path.absname("test/fixtures/image.jpg"),
480 filename: "an_image.jpg"
481 }
482
483 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
484
485 conn =
486 conn
487 |> put_req_header("content-type", "application/json")
488 |> post("/api/v1/statuses", %{
489 "media_ids" => [to_string(upload.id)],
490 "status" => "scheduled",
491 "scheduled_at" => scheduled_at
492 })
493
494 assert %{"media_attachments" => [media_attachment]} =
495 json_response_and_validate_schema(conn, 200)
496
497 assert %{"type" => "image"} = media_attachment
498 end
499
500 test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
501 %{conn: conn} do
502 scheduled_at =
503 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
504 |> NaiveDateTime.to_iso8601()
505 |> Kernel.<>("Z")
506
507 conn =
508 conn
509 |> put_req_header("content-type", "application/json")
510 |> post("/api/v1/statuses", %{
511 "status" => "not scheduled",
512 "scheduled_at" => scheduled_at
513 })
514
515 assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
516 assert [] == Repo.all(ScheduledActivity)
517 end
518
519 test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
520 today =
521 NaiveDateTime.utc_now()
522 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
523 |> NaiveDateTime.to_iso8601()
524 # TODO
525 |> Kernel.<>("Z")
526
527 attrs = %{params: %{}, scheduled_at: today}
528 {:ok, _} = ScheduledActivity.create(user, attrs)
529 {:ok, _} = ScheduledActivity.create(user, attrs)
530
531 conn =
532 conn
533 |> put_req_header("content-type", "application/json")
534 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
535
536 assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
537 end
538
539 test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
540 today =
541 NaiveDateTime.utc_now()
542 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
543 |> NaiveDateTime.to_iso8601()
544 |> Kernel.<>("Z")
545
546 tomorrow =
547 NaiveDateTime.utc_now()
548 |> NaiveDateTime.add(:timer.hours(36), :millisecond)
549 |> NaiveDateTime.to_iso8601()
550 |> Kernel.<>("Z")
551
552 attrs = %{params: %{}, scheduled_at: today}
553 {:ok, _} = ScheduledActivity.create(user, attrs)
554 {:ok, _} = ScheduledActivity.create(user, attrs)
555 {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
556
557 conn =
558 conn
559 |> put_req_header("content-type", "application/json")
560 |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
561
562 assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
563 end
564 end
565
566 describe "posting polls" do
567 setup do: oauth_access(["write:statuses"])
568
569 test "posting a poll", %{conn: conn} do
570 time = NaiveDateTime.utc_now()
571
572 conn =
573 conn
574 |> put_req_header("content-type", "application/json")
575 |> post("/api/v1/statuses", %{
576 "status" => "Who is the #bestgrill?",
577 "poll" => %{
578 "options" => ["Rei", "Asuka", "Misato"],
579 "expires_in" => 420
580 }
581 })
582
583 response = json_response_and_validate_schema(conn, 200)
584
585 assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
586 title in ["Rei", "Asuka", "Misato"]
587 end)
588
589 assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
590 assert response["poll"]["expired"] == false
591
592 question = Object.get_by_id(response["poll"]["id"])
593
594 # closed contains utc timezone
595 assert question.data["closed"] =~ "Z"
596 end
597
598 test "option limit is enforced", %{conn: conn} do
599 limit = Config.get([:instance, :poll_limits, :max_options])
600
601 conn =
602 conn
603 |> put_req_header("content-type", "application/json")
604 |> post("/api/v1/statuses", %{
605 "status" => "desu~",
606 "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
607 })
608
609 %{"error" => error} = json_response_and_validate_schema(conn, 422)
610 assert error == "Poll can't contain more than #{limit} options"
611 end
612
613 test "option character limit is enforced", %{conn: conn} do
614 limit = Config.get([:instance, :poll_limits, :max_option_chars])
615
616 conn =
617 conn
618 |> put_req_header("content-type", "application/json")
619 |> post("/api/v1/statuses", %{
620 "status" => "...",
621 "poll" => %{
622 "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
623 "expires_in" => 1
624 }
625 })
626
627 %{"error" => error} = json_response_and_validate_schema(conn, 422)
628 assert error == "Poll options cannot be longer than #{limit} characters each"
629 end
630
631 test "minimal date limit is enforced", %{conn: conn} do
632 limit = Config.get([:instance, :poll_limits, :min_expiration])
633
634 conn =
635 conn
636 |> put_req_header("content-type", "application/json")
637 |> post("/api/v1/statuses", %{
638 "status" => "imagine arbitrary limits",
639 "poll" => %{
640 "options" => ["this post was made by pleroma gang"],
641 "expires_in" => limit - 1
642 }
643 })
644
645 %{"error" => error} = json_response_and_validate_schema(conn, 422)
646 assert error == "Expiration date is too soon"
647 end
648
649 test "maximum date limit is enforced", %{conn: conn} do
650 limit = Config.get([:instance, :poll_limits, :max_expiration])
651
652 conn =
653 conn
654 |> put_req_header("content-type", "application/json")
655 |> post("/api/v1/statuses", %{
656 "status" => "imagine arbitrary limits",
657 "poll" => %{
658 "options" => ["this post was made by pleroma gang"],
659 "expires_in" => limit + 1
660 }
661 })
662
663 %{"error" => error} = json_response_and_validate_schema(conn, 422)
664 assert error == "Expiration date is too far in the future"
665 end
666
667 test "scheduled poll", %{conn: conn} do
668 clear_config([ScheduledActivity, :enabled], true)
669
670 scheduled_at =
671 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
672 |> NaiveDateTime.to_iso8601()
673 |> Kernel.<>("Z")
674
675 %{"id" => scheduled_id} =
676 conn
677 |> put_req_header("content-type", "application/json")
678 |> post("/api/v1/statuses", %{
679 "status" => "very cool poll",
680 "poll" => %{
681 "options" => ~w(a b c),
682 "expires_in" => 420
683 },
684 "scheduled_at" => scheduled_at
685 })
686 |> json_response_and_validate_schema(200)
687
688 assert {:ok, %{id: activity_id}} =
689 perform_job(Pleroma.Workers.ScheduledActivityWorker, %{
690 activity_id: scheduled_id
691 })
692
693 assert Repo.all(Oban.Job) == []
694
695 object =
696 Activity
697 |> Repo.get(activity_id)
698 |> Object.normalize()
699
700 assert object.data["content"] == "very cool poll"
701 assert object.data["type"] == "Question"
702 assert length(object.data["oneOf"]) == 3
703 end
704 end
705
706 test "get a status" do
707 %{conn: conn} = oauth_access(["read:statuses"])
708 activity = insert(:note_activity)
709
710 conn = get(conn, "/api/v1/statuses/#{activity.id}")
711
712 assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
713 assert id == to_string(activity.id)
714 end
715
716 defp local_and_remote_activities do
717 local = insert(:note_activity)
718 remote = insert(:note_activity, local: false)
719 {:ok, local: local, remote: remote}
720 end
721
722 describe "status with restrict unauthenticated activities for local and remote" do
723 setup do: local_and_remote_activities()
724
725 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
726
727 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
728
729 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
730 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
731
732 assert json_response_and_validate_schema(res_conn, :not_found) == %{
733 "error" => "Record not found"
734 }
735
736 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
737
738 assert json_response_and_validate_schema(res_conn, :not_found) == %{
739 "error" => "Record not found"
740 }
741 end
742
743 test "if user is authenticated", %{local: local, remote: remote} do
744 %{conn: conn} = oauth_access(["read"])
745 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
746 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
747
748 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
749 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
750 end
751 end
752
753 describe "status with restrict unauthenticated activities for local" do
754 setup do: local_and_remote_activities()
755
756 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
757
758 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
759 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
760
761 assert json_response_and_validate_schema(res_conn, :not_found) == %{
762 "error" => "Record not found"
763 }
764
765 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
766 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
767 end
768
769 test "if user is authenticated", %{local: local, remote: remote} do
770 %{conn: conn} = oauth_access(["read"])
771 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
772 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
773
774 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
775 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
776 end
777 end
778
779 describe "status with restrict unauthenticated activities for remote" do
780 setup do: local_and_remote_activities()
781
782 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
783
784 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
785 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
786 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
787
788 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
789
790 assert json_response_and_validate_schema(res_conn, :not_found) == %{
791 "error" => "Record not found"
792 }
793 end
794
795 test "if user is authenticated", %{local: local, remote: remote} do
796 %{conn: conn} = oauth_access(["read"])
797 res_conn = get(conn, "/api/v1/statuses/#{local.id}")
798 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
799
800 res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
801 assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
802 end
803 end
804
805 test "getting a status that doesn't exist returns 404" do
806 %{conn: conn} = oauth_access(["read:statuses"])
807 activity = insert(:note_activity)
808
809 conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
810
811 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
812 end
813
814 test "get a direct status" do
815 %{user: user, conn: conn} = oauth_access(["read:statuses"])
816 other_user = insert(:user)
817
818 {:ok, activity} =
819 CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
820
821 conn =
822 conn
823 |> assign(:user, user)
824 |> get("/api/v1/statuses/#{activity.id}")
825
826 [participation] = Participation.for_user(user)
827
828 res = json_response_and_validate_schema(conn, 200)
829 assert res["pleroma"]["direct_conversation_id"] == participation.id
830 end
831
832 test "get statuses by IDs" do
833 %{conn: conn} = oauth_access(["read:statuses"])
834 %{id: id1} = insert(:note_activity)
835 %{id: id2} = insert(:note_activity)
836
837 query_string = "ids[]=#{id1}&ids[]=#{id2}"
838 conn = get(conn, "/api/v1/statuses/?#{query_string}")
839
840 assert [%{"id" => ^id1}, %{"id" => ^id2}] =
841 Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
842 end
843
844 describe "getting statuses by ids with restricted unauthenticated for local and remote" do
845 setup do: local_and_remote_activities()
846
847 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
848
849 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
850
851 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
852 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
853
854 assert json_response_and_validate_schema(res_conn, 200) == []
855 end
856
857 test "if user is authenticated", %{local: local, remote: remote} do
858 %{conn: conn} = oauth_access(["read"])
859
860 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
861
862 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
863 end
864 end
865
866 describe "getting statuses by ids with restricted unauthenticated for local" do
867 setup do: local_and_remote_activities()
868
869 setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
870
871 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
872 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
873
874 remote_id = remote.id
875 assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
876 end
877
878 test "if user is authenticated", %{local: local, remote: remote} do
879 %{conn: conn} = oauth_access(["read"])
880
881 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
882
883 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
884 end
885 end
886
887 describe "getting statuses by ids with restricted unauthenticated for remote" do
888 setup do: local_and_remote_activities()
889
890 setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
891
892 test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
893 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
894
895 local_id = local.id
896 assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
897 end
898
899 test "if user is authenticated", %{local: local, remote: remote} do
900 %{conn: conn} = oauth_access(["read"])
901
902 res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
903
904 assert length(json_response_and_validate_schema(res_conn, 200)) == 2
905 end
906 end
907
908 describe "deleting a status" do
909 test "when you created it" do
910 %{user: author, conn: conn} = oauth_access(["write:statuses"])
911 activity = insert(:note_activity, user: author)
912 object = Object.normalize(activity, fetch: false)
913
914 content = object.data["content"]
915 source = object.data["source"]
916
917 result =
918 conn
919 |> assign(:user, author)
920 |> delete("/api/v1/statuses/#{activity.id}")
921 |> json_response_and_validate_schema(200)
922
923 assert match?(%{"content" => ^content, "text" => ^source}, result)
924
925 refute Activity.get_by_id(activity.id)
926 end
927
928 test "when it doesn't exist" do
929 %{user: author, conn: conn} = oauth_access(["write:statuses"])
930 activity = insert(:note_activity, user: author)
931
932 conn =
933 conn
934 |> assign(:user, author)
935 |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
936
937 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
938 end
939
940 test "when you didn't create it" do
941 %{conn: conn} = oauth_access(["write:statuses"])
942 activity = insert(:note_activity)
943
944 conn = delete(conn, "/api/v1/statuses/#{activity.id}")
945
946 assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
947
948 assert Activity.get_by_id(activity.id) == activity
949 end
950
951 test "when you're an admin or moderator", %{conn: conn} do
952 activity1 = insert(:note_activity)
953 activity2 = insert(:note_activity)
954 admin = insert(:user, is_admin: true)
955 moderator = insert(:user, is_moderator: true)
956
957 res_conn =
958 conn
959 |> assign(:user, admin)
960 |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
961 |> delete("/api/v1/statuses/#{activity1.id}")
962
963 assert %{} = json_response_and_validate_schema(res_conn, 200)
964
965 res_conn =
966 conn
967 |> assign(:user, moderator)
968 |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
969 |> delete("/api/v1/statuses/#{activity2.id}")
970
971 assert %{} = json_response_and_validate_schema(res_conn, 200)
972
973 refute Activity.get_by_id(activity1.id)
974 refute Activity.get_by_id(activity2.id)
975 end
976 end
977
978 describe "reblogging" do
979 setup do: oauth_access(["write:statuses"])
980
981 test "reblogs and returns the reblogged status", %{conn: conn} do
982 activity = insert(:note_activity)
983
984 conn =
985 conn
986 |> put_req_header("content-type", "application/json")
987 |> post("/api/v1/statuses/#{activity.id}/reblog")
988
989 assert %{
990 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
991 "reblogged" => true
992 } = json_response_and_validate_schema(conn, 200)
993
994 assert to_string(activity.id) == id
995 end
996
997 test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
998 activity = insert(:note_activity)
999
1000 conn =
1001 conn
1002 |> put_req_header("content-type", "application/json")
1003 |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
1004
1005 assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
1006 end
1007
1008 test "reblogs privately and returns the reblogged status", %{conn: conn} do
1009 activity = insert(:note_activity)
1010
1011 conn =
1012 conn
1013 |> put_req_header("content-type", "application/json")
1014 |> post(
1015 "/api/v1/statuses/#{activity.id}/reblog",
1016 %{"visibility" => "private"}
1017 )
1018
1019 assert %{
1020 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1021 "reblogged" => true,
1022 "visibility" => "private"
1023 } = json_response_and_validate_schema(conn, 200)
1024
1025 assert to_string(activity.id) == id
1026 end
1027
1028 test "reblogged status for another user" do
1029 activity = insert(:note_activity)
1030 user1 = insert(:user)
1031 user2 = insert(:user)
1032 user3 = insert(:user)
1033 {:ok, _} = CommonAPI.favorite(user2, activity.id)
1034 {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
1035 {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
1036 {:ok, _} = CommonAPI.repeat(activity.id, user2)
1037
1038 conn_res =
1039 build_conn()
1040 |> assign(:user, user3)
1041 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1042 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1043
1044 assert %{
1045 "reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
1046 "reblogged" => false,
1047 "favourited" => false,
1048 "bookmarked" => false
1049 } = json_response_and_validate_schema(conn_res, 200)
1050
1051 conn_res =
1052 build_conn()
1053 |> assign(:user, user2)
1054 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
1055 |> get("/api/v1/statuses/#{reblog_activity1.id}")
1056
1057 assert %{
1058 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
1059 "reblogged" => true,
1060 "favourited" => true,
1061 "bookmarked" => true
1062 } = json_response_and_validate_schema(conn_res, 200)
1063
1064 assert to_string(activity.id) == id
1065 end
1066
1067 test "author can reblog own private status", %{conn: conn, user: user} do
1068 {:ok, activity} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
1069
1070 conn =
1071 conn
1072 |> put_req_header("content-type", "application/json")
1073 |> post("/api/v1/statuses/#{activity.id}/reblog")
1074
1075 assert %{
1076 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
1077 "reblogged" => true,
1078 "visibility" => "private"
1079 } = json_response_and_validate_schema(conn, 200)
1080
1081 assert to_string(activity.id) == id
1082 end
1083 end
1084
1085 describe "unreblogging" do
1086 setup do: oauth_access(["write:statuses"])
1087
1088 test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
1089 activity = insert(:note_activity)
1090
1091 {:ok, _} = CommonAPI.repeat(activity.id, user)
1092
1093 conn =
1094 conn
1095 |> put_req_header("content-type", "application/json")
1096 |> post("/api/v1/statuses/#{activity.id}/unreblog")
1097
1098 assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
1099 json_response_and_validate_schema(conn, 200)
1100
1101 assert to_string(activity.id) == id
1102 end
1103
1104 test "returns 404 error when activity does not exist", %{conn: conn} do
1105 conn =
1106 conn
1107 |> put_req_header("content-type", "application/json")
1108 |> post("/api/v1/statuses/foo/unreblog")
1109
1110 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1111 end
1112 end
1113
1114 describe "favoriting" do
1115 setup do: oauth_access(["write:favourites"])
1116
1117 test "favs a status and returns it", %{conn: conn} do
1118 activity = insert(:note_activity)
1119
1120 conn =
1121 conn
1122 |> put_req_header("content-type", "application/json")
1123 |> post("/api/v1/statuses/#{activity.id}/favourite")
1124
1125 assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
1126 json_response_and_validate_schema(conn, 200)
1127
1128 assert to_string(activity.id) == id
1129 end
1130
1131 test "favoriting twice will just return 200", %{conn: conn} do
1132 activity = insert(:note_activity)
1133
1134 conn
1135 |> put_req_header("content-type", "application/json")
1136 |> post("/api/v1/statuses/#{activity.id}/favourite")
1137
1138 assert conn
1139 |> put_req_header("content-type", "application/json")
1140 |> post("/api/v1/statuses/#{activity.id}/favourite")
1141 |> json_response_and_validate_schema(200)
1142 end
1143
1144 test "returns 404 error for a wrong id", %{conn: conn} do
1145 conn =
1146 conn
1147 |> put_req_header("content-type", "application/json")
1148 |> post("/api/v1/statuses/1/favourite")
1149
1150 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1151 end
1152 end
1153
1154 describe "unfavoriting" do
1155 setup do: oauth_access(["write:favourites"])
1156
1157 test "unfavorites a status and returns it", %{user: user, conn: conn} do
1158 activity = insert(:note_activity)
1159
1160 {:ok, _} = CommonAPI.favorite(user, activity.id)
1161
1162 conn =
1163 conn
1164 |> put_req_header("content-type", "application/json")
1165 |> post("/api/v1/statuses/#{activity.id}/unfavourite")
1166
1167 assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
1168 json_response_and_validate_schema(conn, 200)
1169
1170 assert to_string(activity.id) == id
1171 end
1172
1173 test "returns 404 error for a wrong id", %{conn: conn} do
1174 conn =
1175 conn
1176 |> put_req_header("content-type", "application/json")
1177 |> post("/api/v1/statuses/1/unfavourite")
1178
1179 assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
1180 end
1181 end
1182
1183 describe "pinned statuses" do
1184 setup do: oauth_access(["write:accounts"])
1185
1186 setup %{user: user} do
1187 {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
1188
1189 %{activity: activity}
1190 end
1191
1192 setup do: clear_config([:instance, :max_pinned_statuses], 1)
1193
1194 test "pin status", %{conn: conn, user: user, activity: activity} do
1195 id_str = to_string(activity.id)
1196
1197 assert %{"id" => ^id_str, "pinned" => true} =
1198 conn
1199 |> put_req_header("content-type", "application/json")
1200 |> post("/api/v1/statuses/#{activity.id}/pin")
1201 |> json_response_and_validate_schema(200)
1202
1203 assert [%{"id" => ^id_str, "pinned" => true}] =
1204 conn
1205 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1206 |> json_response_and_validate_schema(200)
1207 end
1208
1209 test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
1210 {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
1211
1212 conn =
1213 conn
1214 |> put_req_header("content-type", "application/json")
1215 |> post("/api/v1/statuses/#{dm.id}/pin")
1216
1217 assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not pin"}
1218 end
1219
1220 test "unpin status", %{conn: conn, user: user, activity: activity} do
1221 {:ok, _} = CommonAPI.pin(activity.id, user)
1222 user = refresh_record(user)
1223
1224 id_str = to_string(activity.id)
1225
1226 assert %{"id" => ^id_str, "pinned" => false} =
1227 conn
1228 |> assign(:user, user)
1229 |> post("/api/v1/statuses/#{activity.id}/unpin")
1230 |> json_response_and_validate_schema(200)
1231
1232 assert [] =
1233 conn
1234 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
1235 |> json_response_and_validate_schema(200)
1236 end
1237
1238 test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
1239 conn =
1240 conn
1241 |> put_req_header("content-type", "application/json")
1242 |> post("/api/v1/statuses/1/unpin")
1243
1244 assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not unpin"}
1245 end
1246
1247 test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
1248 {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
1249
1250 id_str_one = to_string(activity_one.id)
1251
1252 assert %{"id" => ^id_str_one, "pinned" => true} =
1253 conn
1254 |> put_req_header("content-type", "application/json")
1255 |> post("/api/v1/statuses/#{id_str_one}/pin")
1256 |> json_response_and_validate_schema(200)
1257
1258 user = refresh_record(user)
1259
1260 assert %{"error" => "You have already pinned the maximum number of statuses"} =
1261 conn
1262 |> assign(:user, user)
1263 |> post("/api/v1/statuses/#{activity_two.id}/pin")
1264 |> json_response_and_validate_schema(400)
1265 end
1266
1267 test "on pin removes deletion job, on unpin reschedule deletion" do
1268 %{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
1269 expires_in = 2 * 60 * 60
1270
1271 expires_at = DateTime.add(DateTime.utc_now(), expires_in)
1272
1273 assert %{"id" => id} =
1274 conn
1275 |> put_req_header("content-type", "application/json")
1276 |> post("api/v1/statuses", %{
1277 "status" => "oolong",
1278 "expires_in" => expires_in
1279 })
1280 |> json_response_and_validate_schema(200)
1281
1282 assert_enqueued(
1283 worker: Pleroma.Workers.PurgeExpiredActivity,
1284 args: %{activity_id: id},
1285 scheduled_at: expires_at
1286 )
1287
1288 assert %{"id" => ^id, "pinned" => true} =
1289 conn
1290 |> put_req_header("content-type", "application/json")
1291 |> post("/api/v1/statuses/#{id}/pin")
1292 |> json_response_and_validate_schema(200)
1293
1294 refute_enqueued(
1295 worker: Pleroma.Workers.PurgeExpiredActivity,
1296 args: %{activity_id: id},
1297 scheduled_at: expires_at
1298 )
1299
1300 assert %{"id" => ^id, "pinned" => false} =
1301 conn
1302 |> put_req_header("content-type", "application/json")
1303 |> post("/api/v1/statuses/#{id}/unpin")
1304 |> json_response_and_validate_schema(200)
1305
1306 assert_enqueued(
1307 worker: Pleroma.Workers.PurgeExpiredActivity,
1308 args: %{activity_id: id},
1309 scheduled_at: expires_at
1310 )
1311 end
1312 end
1313
1314 describe "cards" do
1315 setup do
1316 clear_config([:rich_media, :enabled], true)
1317
1318 oauth_access(["read:statuses"])
1319 end
1320
1321 test "returns rich-media card", %{conn: conn, user: user} do
1322 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
1323
1324 {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"})
1325
1326 card_data = %{
1327 "image" => "http://ia.media-imdb.com/images/rock.jpg",
1328 "provider_name" => "example.com",
1329 "provider_url" => "https://example.com",
1330 "title" => "The Rock",
1331 "type" => "link",
1332 "url" => "https://example.com/ogp",
1333 "description" =>
1334 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
1335 "pleroma" => %{
1336 "opengraph" => %{
1337 "image" => "http://ia.media-imdb.com/images/rock.jpg",
1338 "title" => "The Rock",
1339 "type" => "video.movie",
1340 "url" => "https://example.com/ogp",
1341 "description" =>
1342 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
1343 }
1344 }
1345 }
1346
1347 response =
1348 conn
1349 |> get("/api/v1/statuses/#{activity.id}/card")
1350 |> json_response_and_validate_schema(200)
1351
1352 assert response == card_data
1353
1354 # works with private posts
1355 {:ok, activity} =
1356 CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"})
1357
1358 response_two =
1359 conn
1360 |> get("/api/v1/statuses/#{activity.id}/card")
1361 |> json_response_and_validate_schema(200)
1362
1363 assert response_two == card_data
1364 end
1365
1366 test "replaces missing description with an empty string", %{conn: conn, user: user} do
1367 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
1368
1369 {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"})
1370
1371 response =
1372 conn
1373 |> get("/api/v1/statuses/#{activity.id}/card")
1374 |> json_response_and_validate_schema(:ok)
1375
1376 assert response == %{
1377 "type" => "link",
1378 "title" => "Pleroma",
1379 "description" => "",
1380 "image" => nil,
1381 "provider_name" => "example.com",
1382 "provider_url" => "https://example.com",
1383 "url" => "https://example.com/ogp-missing-data",
1384 "pleroma" => %{
1385 "opengraph" => %{
1386 "title" => "Pleroma",
1387 "type" => "website",
1388 "url" => "https://example.com/ogp-missing-data"
1389 }
1390 }
1391 }
1392 end
1393 end
1394
1395 test "bookmarks" do
1396 bookmarks_uri = "/api/v1/bookmarks"
1397
1398 %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
1399 author = insert(:user)
1400
1401 {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
1402 {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
1403
1404 response1 =
1405 conn
1406 |> put_req_header("content-type", "application/json")
1407 |> post("/api/v1/statuses/#{activity1.id}/bookmark")
1408
1409 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
1410
1411 response2 =
1412 conn
1413 |> put_req_header("content-type", "application/json")
1414 |> post("/api/v1/statuses/#{activity2.id}/bookmark")
1415
1416 assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
1417
1418 bookmarks = get(conn, bookmarks_uri)
1419
1420 assert [
1421 json_response_and_validate_schema(response2, 200),
1422 json_response_and_validate_schema(response1, 200)
1423 ] ==
1424 json_response_and_validate_schema(bookmarks, 200)
1425
1426 response1 =
1427 conn
1428 |> put_req_header("content-type", "application/json")
1429 |> post("/api/v1/statuses/#{activity1.id}/unbookmark")
1430
1431 assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
1432
1433 bookmarks = get(conn, bookmarks_uri)
1434
1435 assert [json_response_and_validate_schema(response2, 200)] ==
1436 json_response_and_validate_schema(bookmarks, 200)
1437 end
1438
1439 describe "conversation muting" do
1440 setup do: oauth_access(["write:mutes"])
1441
1442 setup do
1443 post_user = insert(:user)
1444 {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
1445 %{activity: activity}
1446 end
1447
1448 test "mute conversation", %{conn: conn, activity: activity} do
1449 id_str = to_string(activity.id)
1450
1451 assert %{"id" => ^id_str, "muted" => true} =
1452 conn
1453 |> put_req_header("content-type", "application/json")
1454 |> post("/api/v1/statuses/#{activity.id}/mute")
1455 |> json_response_and_validate_schema(200)
1456 end
1457
1458 test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
1459 {:ok, _} = CommonAPI.add_mute(user, activity)
1460
1461 conn =
1462 conn
1463 |> put_req_header("content-type", "application/json")
1464 |> post("/api/v1/statuses/#{activity.id}/mute")
1465
1466 assert json_response_and_validate_schema(conn, 400) == %{
1467 "error" => "conversation is already muted"
1468 }
1469 end
1470
1471 test "unmute conversation", %{conn: conn, user: user, activity: activity} do
1472 {:ok, _} = CommonAPI.add_mute(user, activity)
1473
1474 id_str = to_string(activity.id)
1475
1476 assert %{"id" => ^id_str, "muted" => false} =
1477 conn
1478 # |> assign(:user, user)
1479 |> post("/api/v1/statuses/#{activity.id}/unmute")
1480 |> json_response_and_validate_schema(200)
1481 end
1482 end
1483
1484 test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
1485 user1 = insert(:user)
1486 user2 = insert(:user)
1487 user3 = insert(:user)
1488
1489 {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
1490
1491 # Reply to status from another user
1492 conn1 =
1493 conn
1494 |> assign(:user, user2)
1495 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
1496 |> put_req_header("content-type", "application/json")
1497 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
1498
1499 assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
1500
1501 activity = Activity.get_by_id_with_object(id)
1502
1503 assert Object.normalize(activity, fetch: false).data["inReplyTo"] ==
1504 Object.normalize(replied_to, fetch: false).data["id"]
1505
1506 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
1507
1508 # Reblog from the third user
1509 conn2 =
1510 conn
1511 |> assign(:user, user3)
1512 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
1513 |> put_req_header("content-type", "application/json")
1514 |> post("/api/v1/statuses/#{activity.id}/reblog")
1515
1516 assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
1517 json_response_and_validate_schema(conn2, 200)
1518
1519 assert to_string(activity.id) == id
1520
1521 # Getting third user status
1522 conn3 =
1523 conn
1524 |> assign(:user, user3)
1525 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1526 |> get("api/v1/timelines/home")
1527
1528 [reblogged_activity] = json_response(conn3, 200)
1529
1530 assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
1531
1532 replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
1533 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
1534 end
1535
1536 describe "GET /api/v1/statuses/:id/favourited_by" do
1537 setup do: oauth_access(["read:accounts"])
1538
1539 setup %{user: user} do
1540 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1541
1542 %{activity: activity}
1543 end
1544
1545 test "returns users who have favorited the status", %{conn: conn, activity: activity} do
1546 other_user = insert(:user)
1547 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1548
1549 response =
1550 conn
1551 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1552 |> json_response_and_validate_schema(:ok)
1553
1554 [%{"id" => id}] = response
1555
1556 assert id == other_user.id
1557 end
1558
1559 test "returns empty array when status has not been favorited yet", %{
1560 conn: conn,
1561 activity: activity
1562 } do
1563 response =
1564 conn
1565 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1566 |> json_response_and_validate_schema(:ok)
1567
1568 assert Enum.empty?(response)
1569 end
1570
1571 test "does not return users who have favorited the status but are blocked", %{
1572 conn: %{assigns: %{user: user}} = conn,
1573 activity: activity
1574 } do
1575 other_user = insert(:user)
1576 {:ok, _user_relationship} = User.block(user, other_user)
1577
1578 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1579
1580 response =
1581 conn
1582 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1583 |> json_response_and_validate_schema(:ok)
1584
1585 assert Enum.empty?(response)
1586 end
1587
1588 test "does not fail on an unauthenticated request", %{activity: activity} do
1589 other_user = insert(:user)
1590 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1591
1592 response =
1593 build_conn()
1594 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1595 |> json_response_and_validate_schema(:ok)
1596
1597 [%{"id" => id}] = response
1598 assert id == other_user.id
1599 end
1600
1601 test "requires authentication for private posts", %{user: user} do
1602 other_user = insert(:user)
1603
1604 {:ok, activity} =
1605 CommonAPI.post(user, %{
1606 status: "@#{other_user.nickname} wanna get some #cofe together?",
1607 visibility: "direct"
1608 })
1609
1610 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1611
1612 favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
1613
1614 build_conn()
1615 |> get(favourited_by_url)
1616 |> json_response_and_validate_schema(404)
1617
1618 conn =
1619 build_conn()
1620 |> assign(:user, other_user)
1621 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1622
1623 conn
1624 |> assign(:token, nil)
1625 |> get(favourited_by_url)
1626 |> json_response_and_validate_schema(404)
1627
1628 response =
1629 conn
1630 |> get(favourited_by_url)
1631 |> json_response_and_validate_schema(200)
1632
1633 [%{"id" => id}] = response
1634 assert id == other_user.id
1635 end
1636
1637 test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
1638 clear_config([:instance, :show_reactions], false)
1639
1640 other_user = insert(:user)
1641 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1642
1643 response =
1644 conn
1645 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1646 |> json_response_and_validate_schema(:ok)
1647
1648 assert Enum.empty?(response)
1649 end
1650 end
1651
1652 describe "GET /api/v1/statuses/:id/reblogged_by" do
1653 setup do: oauth_access(["read:accounts"])
1654
1655 setup %{user: user} do
1656 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1657
1658 %{activity: activity}
1659 end
1660
1661 test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
1662 other_user = insert(:user)
1663 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1664
1665 response =
1666 conn
1667 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1668 |> json_response_and_validate_schema(:ok)
1669
1670 [%{"id" => id}] = response
1671
1672 assert id == other_user.id
1673 end
1674
1675 test "returns empty array when status has not been reblogged yet", %{
1676 conn: conn,
1677 activity: activity
1678 } do
1679 response =
1680 conn
1681 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1682 |> json_response_and_validate_schema(:ok)
1683
1684 assert Enum.empty?(response)
1685 end
1686
1687 test "does not return users who have reblogged the status but are blocked", %{
1688 conn: %{assigns: %{user: user}} = conn,
1689 activity: activity
1690 } do
1691 other_user = insert(:user)
1692 {:ok, _user_relationship} = User.block(user, other_user)
1693
1694 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1695
1696 response =
1697 conn
1698 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1699 |> json_response_and_validate_schema(:ok)
1700
1701 assert Enum.empty?(response)
1702 end
1703
1704 test "does not return users who have reblogged the status privately", %{
1705 conn: conn
1706 } do
1707 other_user = insert(:user)
1708 {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
1709
1710 {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
1711
1712 response =
1713 conn
1714 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1715 |> json_response_and_validate_schema(:ok)
1716
1717 assert Enum.empty?(response)
1718 end
1719
1720 test "does not fail on an unauthenticated request", %{activity: activity} do
1721 other_user = insert(:user)
1722 {:ok, _} = CommonAPI.repeat(activity.id, other_user)
1723
1724 response =
1725 build_conn()
1726 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1727 |> json_response_and_validate_schema(:ok)
1728
1729 [%{"id" => id}] = response
1730 assert id == other_user.id
1731 end
1732
1733 test "requires authentication for private posts", %{user: user} do
1734 other_user = insert(:user)
1735
1736 {:ok, activity} =
1737 CommonAPI.post(user, %{
1738 status: "@#{other_user.nickname} wanna get some #cofe together?",
1739 visibility: "direct"
1740 })
1741
1742 build_conn()
1743 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1744 |> json_response_and_validate_schema(404)
1745
1746 response =
1747 build_conn()
1748 |> assign(:user, other_user)
1749 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1750 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1751 |> json_response_and_validate_schema(200)
1752
1753 assert [] == response
1754 end
1755 end
1756
1757 test "context" do
1758 user = insert(:user)
1759
1760 {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
1761 {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
1762 {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
1763 {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
1764 {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
1765
1766 response =
1767 build_conn()
1768 |> get("/api/v1/statuses/#{id3}/context")
1769 |> json_response_and_validate_schema(:ok)
1770
1771 assert %{
1772 "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
1773 "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
1774 } = response
1775 end
1776
1777 test "favorites paginate correctly" do
1778 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1779 other_user = insert(:user)
1780 {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
1781 {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
1782 {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
1783
1784 {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
1785 {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
1786 {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
1787
1788 result =
1789 conn
1790 |> get("/api/v1/favourites?limit=1")
1791
1792 assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
1793 assert post_id == second_post.id
1794
1795 # Using the header for pagination works correctly
1796 [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
1797 [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next)
1798
1799 assert max_id == third_favorite.id
1800
1801 result =
1802 conn
1803 |> get("/api/v1/favourites?max_id=#{max_id}")
1804
1805 assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
1806 json_response_and_validate_schema(result, 200)
1807
1808 assert first_post_id == first_post.id
1809 assert third_post_id == third_post.id
1810 end
1811
1812 test "returns the favorites of a user" do
1813 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1814 other_user = insert(:user)
1815
1816 {:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
1817 {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
1818
1819 {:ok, last_like} = CommonAPI.favorite(user, activity.id)
1820
1821 first_conn = get(conn, "/api/v1/favourites")
1822
1823 assert [status] = json_response_and_validate_schema(first_conn, 200)
1824 assert status["id"] == to_string(activity.id)
1825
1826 assert [{"link", _link_header}] =
1827 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
1828
1829 # Honours query params
1830 {:ok, second_activity} =
1831 CommonAPI.post(other_user, %{
1832 status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
1833 })
1834
1835 {:ok, _} = CommonAPI.favorite(user, second_activity.id)
1836
1837 second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
1838
1839 assert [second_status] = json_response_and_validate_schema(second_conn, 200)
1840 assert second_status["id"] == to_string(second_activity.id)
1841
1842 third_conn = get(conn, "/api/v1/favourites?limit=0")
1843
1844 assert [] = json_response_and_validate_schema(third_conn, 200)
1845 end
1846
1847 test "expires_at is nil for another user" do
1848 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1849 expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
1850 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
1851
1852 assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
1853 conn
1854 |> get("/api/v1/statuses/#{activity.id}")
1855 |> json_response_and_validate_schema(:ok)
1856
1857 {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
1858 assert DateTime.diff(expires_at, a_expires_at) == 0
1859
1860 %{conn: conn} = oauth_access(["read:statuses"])
1861
1862 assert %{"pleroma" => %{"expires_at" => nil}} =
1863 conn
1864 |> get("/api/v1/statuses/#{activity.id}")
1865 |> json_response_and_validate_schema(:ok)
1866 end
1867
1868 test "posting a local only status" do
1869 %{user: _user, conn: conn} = oauth_access(["write:statuses"])
1870
1871 conn_one =
1872 conn
1873 |> put_req_header("content-type", "application/json")
1874 |> post("/api/v1/statuses", %{
1875 "status" => "cofe",
1876 "visibility" => "local"
1877 })
1878
1879 local = Utils.as_local_public()
1880
1881 assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
1882 json_response(conn_one, 200)
1883
1884 assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
1885 end
1886
1887 describe "muted reactions" do
1888 test "index" do
1889 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1890
1891 other_user = insert(:user)
1892 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1893
1894 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
1895 User.mute(user, other_user)
1896
1897 result =
1898 conn
1899 |> get("/api/v1/statuses/?ids[]=#{activity.id}")
1900 |> json_response_and_validate_schema(200)
1901
1902 assert [
1903 %{
1904 "pleroma" => %{
1905 "emoji_reactions" => []
1906 }
1907 }
1908 ] = result
1909
1910 result =
1911 conn
1912 |> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
1913 |> json_response_and_validate_schema(200)
1914
1915 assert [
1916 %{
1917 "pleroma" => %{
1918 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
1919 }
1920 }
1921 ] = result
1922 end
1923
1924 test "show" do
1925 # %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
1926 %{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
1927
1928 other_user = insert(:user)
1929 {:ok, activity} = CommonAPI.post(user, %{status: "test"})
1930
1931 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
1932 User.mute(user, other_user)
1933
1934 result =
1935 conn
1936 |> get("/api/v1/statuses/#{activity.id}")
1937 |> json_response_and_validate_schema(200)
1938
1939 assert %{
1940 "pleroma" => %{
1941 "emoji_reactions" => []
1942 }
1943 } = result
1944
1945 result =
1946 conn
1947 |> get("/api/v1/statuses/#{activity.id}?with_muted=true")
1948 |> json_response_and_validate_schema(200)
1949
1950 assert %{
1951 "pleroma" => %{
1952 "emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
1953 }
1954 } = result
1955 end
1956 end
1957 end