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