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