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