Merge branch 'develop' into fix/signup-without-email
[akkoma] / test / web / mastodon_api / controllers / status_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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
8 alias Pleroma.Activity
9 alias Pleroma.ActivityExpiration
10 alias Pleroma.Config
11 alias Pleroma.Conversation.Participation
12 alias Pleroma.Object
13 alias Pleroma.Repo
14 alias Pleroma.ScheduledActivity
15 alias Pleroma.Tests.ObanHelpers
16 alias Pleroma.User
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.CommonAPI
19
20 import Pleroma.Factory
21
22 clear_config([:instance, :federating])
23 clear_config([:instance, :allow_relay])
24 clear_config([:rich_media, :enabled])
25
26 describe "posting statuses" do
27 setup do: oauth_access(["write:statuses"])
28
29 test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
30 Pleroma.Config.put([:instance, :federating], true)
31 Pleroma.Config.get([:instance, :allow_relay], true)
32
33 response =
34 conn
35 |> post("api/v1/statuses", %{
36 "content_type" => "text/plain",
37 "source" => "Pleroma FE",
38 "status" => "Hello world",
39 "visibility" => "public"
40 })
41 |> json_response(200)
42
43 assert response["reblogs_count"] == 0
44 ObanHelpers.perform_all()
45
46 response =
47 conn
48 |> get("api/v1/statuses/#{response["id"]}", %{})
49 |> json_response(200)
50
51 assert response["reblogs_count"] == 0
52 end
53
54 test "posting a status", %{conn: conn} do
55 idempotency_key = "Pikachu rocks!"
56
57 conn_one =
58 conn
59 |> put_req_header("idempotency-key", idempotency_key)
60 |> post("/api/v1/statuses", %{
61 "status" => "cofe",
62 "spoiler_text" => "2hu",
63 "sensitive" => "false"
64 })
65
66 {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
67 # Six hours
68 assert ttl > :timer.seconds(6 * 60 * 60 - 1)
69
70 assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
71 json_response(conn_one, 200)
72
73 assert Activity.get_by_id(id)
74
75 conn_two =
76 conn
77 |> put_req_header("idempotency-key", idempotency_key)
78 |> post("/api/v1/statuses", %{
79 "status" => "cofe",
80 "spoiler_text" => "2hu",
81 "sensitive" => "false"
82 })
83
84 assert %{"id" => second_id} = json_response(conn_two, 200)
85 assert id == second_id
86
87 conn_three =
88 conn
89 |> post("/api/v1/statuses", %{
90 "status" => "cofe",
91 "spoiler_text" => "2hu",
92 "sensitive" => "false"
93 })
94
95 assert %{"id" => third_id} = json_response(conn_three, 200)
96 refute id == third_id
97
98 # An activity that will expire:
99 # 2 hours
100 expires_in = 120 * 60
101
102 conn_four =
103 conn
104 |> post("api/v1/statuses", %{
105 "status" => "oolong",
106 "expires_in" => expires_in
107 })
108
109 assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200)
110 assert activity = Activity.get_by_id(fourth_id)
111 assert expiration = ActivityExpiration.get_by_activity_id(fourth_id)
112
113 estimated_expires_at =
114 NaiveDateTime.utc_now()
115 |> NaiveDateTime.add(expires_in)
116 |> NaiveDateTime.truncate(:second)
117
118 # This assert will fail if the test takes longer than a minute. I sure hope it never does:
119 assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60
120
121 assert fourth_response["pleroma"]["expires_at"] ==
122 NaiveDateTime.to_iso8601(expiration.scheduled_at)
123 end
124
125 test "it fails to create a status if `expires_in` is less or equal than an hour", %{
126 conn: conn
127 } do
128 # 1 hour
129 expires_in = 60 * 60
130
131 assert %{"error" => "Expiry date is too soon"} =
132 conn
133 |> post("api/v1/statuses", %{
134 "status" => "oolong",
135 "expires_in" => expires_in
136 })
137 |> json_response(422)
138
139 # 30 minutes
140 expires_in = 30 * 60
141
142 assert %{"error" => "Expiry date is too soon"} =
143 conn
144 |> post("api/v1/statuses", %{
145 "status" => "oolong",
146 "expires_in" => expires_in
147 })
148 |> json_response(422)
149 end
150
151 test "posting an undefined status with an attachment", %{user: user, conn: conn} do
152 file = %Plug.Upload{
153 content_type: "image/jpg",
154 path: Path.absname("test/fixtures/image.jpg"),
155 filename: "an_image.jpg"
156 }
157
158 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
159
160 conn =
161 post(conn, "/api/v1/statuses", %{
162 "media_ids" => [to_string(upload.id)]
163 })
164
165 assert json_response(conn, 200)
166 end
167
168 test "replying to a status", %{user: user, conn: conn} do
169 {:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"})
170
171 conn =
172 conn
173 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
174
175 assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
176
177 activity = Activity.get_by_id(id)
178
179 assert activity.data["context"] == replied_to.data["context"]
180 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
181 end
182
183 test "replying to a direct message with visibility other than direct", %{
184 user: user,
185 conn: conn
186 } do
187 {:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
188
189 Enum.each(["public", "private", "unlisted"], fn visibility ->
190 conn =
191 conn
192 |> post("/api/v1/statuses", %{
193 "status" => "@#{user.nickname} hey",
194 "in_reply_to_id" => replied_to.id,
195 "visibility" => visibility
196 })
197
198 assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"}
199 end)
200 end
201
202 test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
203 conn = post(conn, "/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
204
205 assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
206 assert Activity.get_by_id(id)
207 end
208
209 test "posting a sensitive status", %{conn: conn} do
210 conn = post(conn, "/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
211
212 assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
213 assert Activity.get_by_id(id)
214 end
215
216 test "posting a fake status", %{conn: conn} do
217 real_conn =
218 post(conn, "/api/v1/statuses", %{
219 "status" =>
220 "\"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"
221 })
222
223 real_status = json_response(real_conn, 200)
224
225 assert real_status
226 assert Object.get_by_ap_id(real_status["uri"])
227
228 real_status =
229 real_status
230 |> Map.put("id", nil)
231 |> Map.put("url", nil)
232 |> Map.put("uri", nil)
233 |> Map.put("created_at", nil)
234 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
235
236 fake_conn =
237 post(conn, "/api/v1/statuses", %{
238 "status" =>
239 "\"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",
240 "preview" => true
241 })
242
243 fake_status = json_response(fake_conn, 200)
244
245 assert fake_status
246 refute Object.get_by_ap_id(fake_status["uri"])
247
248 fake_status =
249 fake_status
250 |> Map.put("id", nil)
251 |> Map.put("url", nil)
252 |> Map.put("uri", nil)
253 |> Map.put("created_at", nil)
254 |> Kernel.put_in(["pleroma", "conversation_id"], nil)
255
256 assert real_status == fake_status
257 end
258
259 test "posting a status with OGP link preview", %{conn: conn} do
260 Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
261 Config.put([:rich_media, :enabled], true)
262
263 conn =
264 post(conn, "/api/v1/statuses", %{
265 "status" => "https://example.com/ogp"
266 })
267
268 assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
269 assert Activity.get_by_id(id)
270 end
271
272 test "posting a direct status", %{conn: conn} do
273 user2 = insert(:user)
274 content = "direct cofe @#{user2.nickname}"
275
276 conn = post(conn, "api/v1/statuses", %{"status" => content, "visibility" => "direct"})
277
278 assert %{"id" => id} = response = json_response(conn, 200)
279 assert response["visibility"] == "direct"
280 assert response["pleroma"]["direct_conversation_id"]
281 assert activity = Activity.get_by_id(id)
282 assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
283 assert activity.data["to"] == [user2.ap_id]
284 assert activity.data["cc"] == []
285 end
286 end
287
288 describe "posting scheduled statuses" do
289 setup do: oauth_access(["write:statuses"])
290
291 test "creates a scheduled activity", %{conn: conn} do
292 scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
293
294 conn =
295 post(conn, "/api/v1/statuses", %{
296 "status" => "scheduled",
297 "scheduled_at" => scheduled_at
298 })
299
300 assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200)
301 assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
302 assert [] == Repo.all(Activity)
303 end
304
305 test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
306 scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
307
308 file = %Plug.Upload{
309 content_type: "image/jpg",
310 path: Path.absname("test/fixtures/image.jpg"),
311 filename: "an_image.jpg"
312 }
313
314 {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
315
316 conn =
317 post(conn, "/api/v1/statuses", %{
318 "media_ids" => [to_string(upload.id)],
319 "status" => "scheduled",
320 "scheduled_at" => scheduled_at
321 })
322
323 assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
324 assert %{"type" => "image"} = media_attachment
325 end
326
327 test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
328 %{conn: conn} do
329 scheduled_at =
330 NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
331
332 conn =
333 post(conn, "/api/v1/statuses", %{
334 "status" => "not scheduled",
335 "scheduled_at" => scheduled_at
336 })
337
338 assert %{"content" => "not scheduled"} = json_response(conn, 200)
339 assert [] == Repo.all(ScheduledActivity)
340 end
341
342 test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
343 today =
344 NaiveDateTime.utc_now()
345 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
346 |> NaiveDateTime.to_iso8601()
347
348 attrs = %{params: %{}, scheduled_at: today}
349 {:ok, _} = ScheduledActivity.create(user, attrs)
350 {:ok, _} = ScheduledActivity.create(user, attrs)
351
352 conn = post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
353
354 assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
355 end
356
357 test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
358 today =
359 NaiveDateTime.utc_now()
360 |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
361 |> NaiveDateTime.to_iso8601()
362
363 tomorrow =
364 NaiveDateTime.utc_now()
365 |> NaiveDateTime.add(:timer.hours(36), :millisecond)
366 |> NaiveDateTime.to_iso8601()
367
368 attrs = %{params: %{}, scheduled_at: today}
369 {:ok, _} = ScheduledActivity.create(user, attrs)
370 {:ok, _} = ScheduledActivity.create(user, attrs)
371 {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
372
373 conn =
374 post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
375
376 assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
377 end
378 end
379
380 describe "posting polls" do
381 setup do: oauth_access(["write:statuses"])
382
383 test "posting a poll", %{conn: conn} do
384 time = NaiveDateTime.utc_now()
385
386 conn =
387 post(conn, "/api/v1/statuses", %{
388 "status" => "Who is the #bestgrill?",
389 "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
390 })
391
392 response = json_response(conn, 200)
393
394 assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
395 title in ["Rei", "Asuka", "Misato"]
396 end)
397
398 assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
399 refute response["poll"]["expred"]
400
401 question = Object.get_by_id(response["poll"]["id"])
402
403 # closed contains utc timezone
404 assert question.data["closed"] =~ "Z"
405 end
406
407 test "option limit is enforced", %{conn: conn} do
408 limit = Config.get([:instance, :poll_limits, :max_options])
409
410 conn =
411 post(conn, "/api/v1/statuses", %{
412 "status" => "desu~",
413 "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
414 })
415
416 %{"error" => error} = json_response(conn, 422)
417 assert error == "Poll can't contain more than #{limit} options"
418 end
419
420 test "option character limit is enforced", %{conn: conn} do
421 limit = Config.get([:instance, :poll_limits, :max_option_chars])
422
423 conn =
424 post(conn, "/api/v1/statuses", %{
425 "status" => "...",
426 "poll" => %{
427 "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
428 "expires_in" => 1
429 }
430 })
431
432 %{"error" => error} = json_response(conn, 422)
433 assert error == "Poll options cannot be longer than #{limit} characters each"
434 end
435
436 test "minimal date limit is enforced", %{conn: conn} do
437 limit = Config.get([:instance, :poll_limits, :min_expiration])
438
439 conn =
440 post(conn, "/api/v1/statuses", %{
441 "status" => "imagine arbitrary limits",
442 "poll" => %{
443 "options" => ["this post was made by pleroma gang"],
444 "expires_in" => limit - 1
445 }
446 })
447
448 %{"error" => error} = json_response(conn, 422)
449 assert error == "Expiration date is too soon"
450 end
451
452 test "maximum date limit is enforced", %{conn: conn} do
453 limit = Config.get([:instance, :poll_limits, :max_expiration])
454
455 conn =
456 post(conn, "/api/v1/statuses", %{
457 "status" => "imagine arbitrary limits",
458 "poll" => %{
459 "options" => ["this post was made by pleroma gang"],
460 "expires_in" => limit + 1
461 }
462 })
463
464 %{"error" => error} = json_response(conn, 422)
465 assert error == "Expiration date is too far in the future"
466 end
467 end
468
469 test "get a status" do
470 %{conn: conn} = oauth_access(["read:statuses"])
471 activity = insert(:note_activity)
472
473 conn = get(conn, "/api/v1/statuses/#{activity.id}")
474
475 assert %{"id" => id} = json_response(conn, 200)
476 assert id == to_string(activity.id)
477 end
478
479 test "get a direct status" do
480 %{user: user, conn: conn} = oauth_access(["read:statuses"])
481 other_user = insert(:user)
482
483 {:ok, activity} =
484 CommonAPI.post(user, %{"status" => "@#{other_user.nickname}", "visibility" => "direct"})
485
486 conn =
487 conn
488 |> assign(:user, user)
489 |> get("/api/v1/statuses/#{activity.id}")
490
491 [participation] = Participation.for_user(user)
492
493 res = json_response(conn, 200)
494 assert res["pleroma"]["direct_conversation_id"] == participation.id
495 end
496
497 test "get statuses by IDs" do
498 %{conn: conn} = oauth_access(["read:statuses"])
499 %{id: id1} = insert(:note_activity)
500 %{id: id2} = insert(:note_activity)
501
502 query_string = "ids[]=#{id1}&ids[]=#{id2}"
503 conn = get(conn, "/api/v1/statuses/?#{query_string}")
504
505 assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"])
506 end
507
508 describe "deleting a status" do
509 test "when you created it" do
510 %{user: author, conn: conn} = oauth_access(["write:statuses"])
511 activity = insert(:note_activity, user: author)
512
513 conn =
514 conn
515 |> assign(:user, author)
516 |> delete("/api/v1/statuses/#{activity.id}")
517
518 assert %{} = json_response(conn, 200)
519
520 refute Activity.get_by_id(activity.id)
521 end
522
523 test "when you didn't create it" do
524 %{conn: conn} = oauth_access(["write:statuses"])
525 activity = insert(:note_activity)
526
527 conn = delete(conn, "/api/v1/statuses/#{activity.id}")
528
529 assert %{"error" => _} = json_response(conn, 403)
530
531 assert Activity.get_by_id(activity.id) == activity
532 end
533
534 test "when you're an admin or moderator", %{conn: conn} do
535 activity1 = insert(:note_activity)
536 activity2 = insert(:note_activity)
537 admin = insert(:user, is_admin: true)
538 moderator = insert(:user, is_moderator: true)
539
540 res_conn =
541 conn
542 |> assign(:user, admin)
543 |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
544 |> delete("/api/v1/statuses/#{activity1.id}")
545
546 assert %{} = json_response(res_conn, 200)
547
548 res_conn =
549 conn
550 |> assign(:user, moderator)
551 |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
552 |> delete("/api/v1/statuses/#{activity2.id}")
553
554 assert %{} = json_response(res_conn, 200)
555
556 refute Activity.get_by_id(activity1.id)
557 refute Activity.get_by_id(activity2.id)
558 end
559 end
560
561 describe "reblogging" do
562 setup do: oauth_access(["write:statuses"])
563
564 test "reblogs and returns the reblogged status", %{conn: conn} do
565 activity = insert(:note_activity)
566
567 conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog")
568
569 assert %{
570 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
571 "reblogged" => true
572 } = json_response(conn, 200)
573
574 assert to_string(activity.id) == id
575 end
576
577 test "reblogs privately and returns the reblogged status", %{conn: conn} do
578 activity = insert(:note_activity)
579
580 conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
581
582 assert %{
583 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
584 "reblogged" => true,
585 "visibility" => "private"
586 } = json_response(conn, 200)
587
588 assert to_string(activity.id) == id
589 end
590
591 test "reblogged status for another user" do
592 activity = insert(:note_activity)
593 user1 = insert(:user)
594 user2 = insert(:user)
595 user3 = insert(:user)
596 CommonAPI.favorite(activity.id, user2)
597 {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
598 {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
599 {:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
600
601 conn_res =
602 build_conn()
603 |> assign(:user, user3)
604 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
605 |> get("/api/v1/statuses/#{reblog_activity1.id}")
606
607 assert %{
608 "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
609 "reblogged" => false,
610 "favourited" => false,
611 "bookmarked" => false
612 } = json_response(conn_res, 200)
613
614 conn_res =
615 build_conn()
616 |> assign(:user, user2)
617 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
618 |> get("/api/v1/statuses/#{reblog_activity1.id}")
619
620 assert %{
621 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
622 "reblogged" => true,
623 "favourited" => true,
624 "bookmarked" => true
625 } = json_response(conn_res, 200)
626
627 assert to_string(activity.id) == id
628 end
629
630 test "returns 400 error when activity is not exist", %{conn: conn} do
631 conn = post(conn, "/api/v1/statuses/foo/reblog")
632
633 assert json_response(conn, 400) == %{"error" => "Could not repeat"}
634 end
635 end
636
637 describe "unreblogging" do
638 setup do: oauth_access(["write:statuses"])
639
640 test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
641 activity = insert(:note_activity)
642
643 {:ok, _, _} = CommonAPI.repeat(activity.id, user)
644
645 conn = post(conn, "/api/v1/statuses/#{activity.id}/unreblog")
646
647 assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
648
649 assert to_string(activity.id) == id
650 end
651
652 test "returns 400 error when activity is not exist", %{conn: conn} do
653 conn = post(conn, "/api/v1/statuses/foo/unreblog")
654
655 assert json_response(conn, 400) == %{"error" => "Could not unrepeat"}
656 end
657 end
658
659 describe "favoriting" do
660 setup do: oauth_access(["write:favourites"])
661
662 test "favs a status and returns it", %{conn: conn} do
663 activity = insert(:note_activity)
664
665 conn = post(conn, "/api/v1/statuses/#{activity.id}/favourite")
666
667 assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
668 json_response(conn, 200)
669
670 assert to_string(activity.id) == id
671 end
672
673 test "favoriting twice will just return 200", %{conn: conn} do
674 activity = insert(:note_activity)
675
676 post(conn, "/api/v1/statuses/#{activity.id}/favourite")
677 assert post(conn, "/api/v1/statuses/#{activity.id}/favourite") |> json_response(200)
678 end
679
680 test "returns 400 error for a wrong id", %{conn: conn} do
681 conn = post(conn, "/api/v1/statuses/1/favourite")
682
683 assert json_response(conn, 400) == %{"error" => "Could not favorite"}
684 end
685 end
686
687 describe "unfavoriting" do
688 setup do: oauth_access(["write:favourites"])
689
690 test "unfavorites a status and returns it", %{user: user, conn: conn} do
691 activity = insert(:note_activity)
692
693 {:ok, _, _} = CommonAPI.favorite(activity.id, user)
694
695 conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite")
696
697 assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
698 json_response(conn, 200)
699
700 assert to_string(activity.id) == id
701 end
702
703 test "returns 400 error for a wrong id", %{conn: conn} do
704 conn = post(conn, "/api/v1/statuses/1/unfavourite")
705
706 assert json_response(conn, 400) == %{"error" => "Could not unfavorite"}
707 end
708 end
709
710 describe "pinned statuses" do
711 setup do: oauth_access(["write:accounts"])
712
713 setup %{user: user} do
714 {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
715
716 %{activity: activity}
717 end
718
719 clear_config([:instance, :max_pinned_statuses]) do
720 Config.put([:instance, :max_pinned_statuses], 1)
721 end
722
723 test "pin status", %{conn: conn, user: user, activity: activity} do
724 id_str = to_string(activity.id)
725
726 assert %{"id" => ^id_str, "pinned" => true} =
727 conn
728 |> post("/api/v1/statuses/#{activity.id}/pin")
729 |> json_response(200)
730
731 assert [%{"id" => ^id_str, "pinned" => true}] =
732 conn
733 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
734 |> json_response(200)
735 end
736
737 test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
738 {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
739
740 conn = post(conn, "/api/v1/statuses/#{dm.id}/pin")
741
742 assert json_response(conn, 400) == %{"error" => "Could not pin"}
743 end
744
745 test "unpin status", %{conn: conn, user: user, activity: activity} do
746 {:ok, _} = CommonAPI.pin(activity.id, user)
747 user = refresh_record(user)
748
749 id_str = to_string(activity.id)
750
751 assert %{"id" => ^id_str, "pinned" => false} =
752 conn
753 |> assign(:user, user)
754 |> post("/api/v1/statuses/#{activity.id}/unpin")
755 |> json_response(200)
756
757 assert [] =
758 conn
759 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
760 |> json_response(200)
761 end
762
763 test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
764 conn = post(conn, "/api/v1/statuses/1/unpin")
765
766 assert json_response(conn, 400) == %{"error" => "Could not unpin"}
767 end
768
769 test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
770 {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
771
772 id_str_one = to_string(activity_one.id)
773
774 assert %{"id" => ^id_str_one, "pinned" => true} =
775 conn
776 |> post("/api/v1/statuses/#{id_str_one}/pin")
777 |> json_response(200)
778
779 user = refresh_record(user)
780
781 assert %{"error" => "You have already pinned the maximum number of statuses"} =
782 conn
783 |> assign(:user, user)
784 |> post("/api/v1/statuses/#{activity_two.id}/pin")
785 |> json_response(400)
786 end
787 end
788
789 describe "cards" do
790 setup do
791 Config.put([:rich_media, :enabled], true)
792
793 oauth_access(["read:statuses"])
794 end
795
796 test "returns rich-media card", %{conn: conn, user: user} do
797 Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
798
799 {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"})
800
801 card_data = %{
802 "image" => "http://ia.media-imdb.com/images/rock.jpg",
803 "provider_name" => "example.com",
804 "provider_url" => "https://example.com",
805 "title" => "The Rock",
806 "type" => "link",
807 "url" => "https://example.com/ogp",
808 "description" =>
809 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
810 "pleroma" => %{
811 "opengraph" => %{
812 "image" => "http://ia.media-imdb.com/images/rock.jpg",
813 "title" => "The Rock",
814 "type" => "video.movie",
815 "url" => "https://example.com/ogp",
816 "description" =>
817 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
818 }
819 }
820 }
821
822 response =
823 conn
824 |> get("/api/v1/statuses/#{activity.id}/card")
825 |> json_response(200)
826
827 assert response == card_data
828
829 # works with private posts
830 {:ok, activity} =
831 CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"})
832
833 response_two =
834 conn
835 |> get("/api/v1/statuses/#{activity.id}/card")
836 |> json_response(200)
837
838 assert response_two == card_data
839 end
840
841 test "replaces missing description with an empty string", %{conn: conn, user: user} do
842 Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
843
844 {:ok, activity} =
845 CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"})
846
847 response =
848 conn
849 |> get("/api/v1/statuses/#{activity.id}/card")
850 |> json_response(:ok)
851
852 assert response == %{
853 "type" => "link",
854 "title" => "Pleroma",
855 "description" => "",
856 "image" => nil,
857 "provider_name" => "example.com",
858 "provider_url" => "https://example.com",
859 "url" => "https://example.com/ogp-missing-data",
860 "pleroma" => %{
861 "opengraph" => %{
862 "title" => "Pleroma",
863 "type" => "website",
864 "url" => "https://example.com/ogp-missing-data"
865 }
866 }
867 }
868 end
869 end
870
871 test "bookmarks" do
872 %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
873 author = insert(:user)
874
875 {:ok, activity1} =
876 CommonAPI.post(author, %{
877 "status" => "heweoo?"
878 })
879
880 {:ok, activity2} =
881 CommonAPI.post(author, %{
882 "status" => "heweoo!"
883 })
884
885 response1 = post(conn, "/api/v1/statuses/#{activity1.id}/bookmark")
886
887 assert json_response(response1, 200)["bookmarked"] == true
888
889 response2 = post(conn, "/api/v1/statuses/#{activity2.id}/bookmark")
890
891 assert json_response(response2, 200)["bookmarked"] == true
892
893 bookmarks = get(conn, "/api/v1/bookmarks")
894
895 assert [json_response(response2, 200), json_response(response1, 200)] ==
896 json_response(bookmarks, 200)
897
898 response1 = post(conn, "/api/v1/statuses/#{activity1.id}/unbookmark")
899
900 assert json_response(response1, 200)["bookmarked"] == false
901
902 bookmarks = get(conn, "/api/v1/bookmarks")
903
904 assert [json_response(response2, 200)] == json_response(bookmarks, 200)
905 end
906
907 describe "conversation muting" do
908 setup do: oauth_access(["write:mutes"])
909
910 setup do
911 post_user = insert(:user)
912 {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"})
913 %{activity: activity}
914 end
915
916 test "mute conversation", %{conn: conn, activity: activity} do
917 id_str = to_string(activity.id)
918
919 assert %{"id" => ^id_str, "muted" => true} =
920 conn
921 |> post("/api/v1/statuses/#{activity.id}/mute")
922 |> json_response(200)
923 end
924
925 test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
926 {:ok, _} = CommonAPI.add_mute(user, activity)
927
928 conn = post(conn, "/api/v1/statuses/#{activity.id}/mute")
929
930 assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
931 end
932
933 test "unmute conversation", %{conn: conn, user: user, activity: activity} do
934 {:ok, _} = CommonAPI.add_mute(user, activity)
935
936 id_str = to_string(activity.id)
937
938 assert %{"id" => ^id_str, "muted" => false} =
939 conn
940 # |> assign(:user, user)
941 |> post("/api/v1/statuses/#{activity.id}/unmute")
942 |> json_response(200)
943 end
944 end
945
946 test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
947 user1 = insert(:user)
948 user2 = insert(:user)
949 user3 = insert(:user)
950
951 {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
952
953 # Reply to status from another user
954 conn1 =
955 conn
956 |> assign(:user, user2)
957 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
958 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
959
960 assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
961
962 activity = Activity.get_by_id_with_object(id)
963
964 assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
965 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
966
967 # Reblog from the third user
968 conn2 =
969 conn
970 |> assign(:user, user3)
971 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
972 |> post("/api/v1/statuses/#{activity.id}/reblog")
973
974 assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
975 json_response(conn2, 200)
976
977 assert to_string(activity.id) == id
978
979 # Getting third user status
980 conn3 =
981 conn
982 |> assign(:user, user3)
983 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
984 |> get("api/v1/timelines/home")
985
986 [reblogged_activity] = json_response(conn3, 200)
987
988 assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
989
990 replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
991 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
992 end
993
994 describe "GET /api/v1/statuses/:id/favourited_by" do
995 setup do: oauth_access(["read:accounts"])
996
997 setup %{user: user} do
998 {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
999
1000 %{activity: activity}
1001 end
1002
1003 test "returns users who have favorited the status", %{conn: conn, activity: activity} do
1004 other_user = insert(:user)
1005 {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
1006
1007 response =
1008 conn
1009 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1010 |> json_response(:ok)
1011
1012 [%{"id" => id}] = response
1013
1014 assert id == other_user.id
1015 end
1016
1017 test "returns empty array when status has not been favorited yet", %{
1018 conn: conn,
1019 activity: activity
1020 } do
1021 response =
1022 conn
1023 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1024 |> json_response(:ok)
1025
1026 assert Enum.empty?(response)
1027 end
1028
1029 test "does not return users who have favorited the status but are blocked", %{
1030 conn: %{assigns: %{user: user}} = conn,
1031 activity: activity
1032 } do
1033 other_user = insert(:user)
1034 {:ok, _user_relationship} = User.block(user, other_user)
1035
1036 {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
1037
1038 response =
1039 conn
1040 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1041 |> json_response(:ok)
1042
1043 assert Enum.empty?(response)
1044 end
1045
1046 test "does not fail on an unauthenticated request", %{activity: activity} do
1047 other_user = insert(:user)
1048 {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
1049
1050 response =
1051 build_conn()
1052 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1053 |> json_response(:ok)
1054
1055 [%{"id" => id}] = response
1056 assert id == other_user.id
1057 end
1058
1059 test "requires authentication for private posts", %{user: user} do
1060 other_user = insert(:user)
1061
1062 {:ok, activity} =
1063 CommonAPI.post(user, %{
1064 "status" => "@#{other_user.nickname} wanna get some #cofe together?",
1065 "visibility" => "direct"
1066 })
1067
1068 {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
1069
1070 favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
1071
1072 build_conn()
1073 |> get(favourited_by_url)
1074 |> json_response(404)
1075
1076 conn =
1077 build_conn()
1078 |> assign(:user, other_user)
1079 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1080
1081 conn
1082 |> assign(:token, nil)
1083 |> get(favourited_by_url)
1084 |> json_response(404)
1085
1086 response =
1087 conn
1088 |> get(favourited_by_url)
1089 |> json_response(200)
1090
1091 [%{"id" => id}] = response
1092 assert id == other_user.id
1093 end
1094 end
1095
1096 describe "GET /api/v1/statuses/:id/reblogged_by" do
1097 setup do: oauth_access(["read:accounts"])
1098
1099 setup %{user: user} do
1100 {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
1101
1102 %{activity: activity}
1103 end
1104
1105 test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
1106 other_user = insert(:user)
1107 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1108
1109 response =
1110 conn
1111 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1112 |> json_response(:ok)
1113
1114 [%{"id" => id}] = response
1115
1116 assert id == other_user.id
1117 end
1118
1119 test "returns empty array when status has not been reblogged yet", %{
1120 conn: conn,
1121 activity: activity
1122 } do
1123 response =
1124 conn
1125 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1126 |> json_response(:ok)
1127
1128 assert Enum.empty?(response)
1129 end
1130
1131 test "does not return users who have reblogged the status but are blocked", %{
1132 conn: %{assigns: %{user: user}} = conn,
1133 activity: activity
1134 } do
1135 other_user = insert(:user)
1136 {:ok, _user_relationship} = User.block(user, other_user)
1137
1138 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1139
1140 response =
1141 conn
1142 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1143 |> json_response(:ok)
1144
1145 assert Enum.empty?(response)
1146 end
1147
1148 test "does not return users who have reblogged the status privately", %{
1149 conn: conn,
1150 activity: activity
1151 } do
1152 other_user = insert(:user)
1153
1154 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user, %{"visibility" => "private"})
1155
1156 response =
1157 conn
1158 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1159 |> json_response(:ok)
1160
1161 assert Enum.empty?(response)
1162 end
1163
1164 test "does not fail on an unauthenticated request", %{activity: activity} do
1165 other_user = insert(:user)
1166 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1167
1168 response =
1169 build_conn()
1170 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1171 |> json_response(:ok)
1172
1173 [%{"id" => id}] = response
1174 assert id == other_user.id
1175 end
1176
1177 test "requires authentication for private posts", %{user: user} do
1178 other_user = insert(:user)
1179
1180 {:ok, activity} =
1181 CommonAPI.post(user, %{
1182 "status" => "@#{other_user.nickname} wanna get some #cofe together?",
1183 "visibility" => "direct"
1184 })
1185
1186 build_conn()
1187 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1188 |> json_response(404)
1189
1190 response =
1191 build_conn()
1192 |> assign(:user, other_user)
1193 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1194 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1195 |> json_response(200)
1196
1197 assert [] == response
1198 end
1199 end
1200
1201 test "context" do
1202 user = insert(:user)
1203
1204 {:ok, %{id: id1}} = CommonAPI.post(user, %{"status" => "1"})
1205 {:ok, %{id: id2}} = CommonAPI.post(user, %{"status" => "2", "in_reply_to_status_id" => id1})
1206 {:ok, %{id: id3}} = CommonAPI.post(user, %{"status" => "3", "in_reply_to_status_id" => id2})
1207 {:ok, %{id: id4}} = CommonAPI.post(user, %{"status" => "4", "in_reply_to_status_id" => id3})
1208 {:ok, %{id: id5}} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4})
1209
1210 response =
1211 build_conn()
1212 |> get("/api/v1/statuses/#{id3}/context")
1213 |> json_response(:ok)
1214
1215 assert %{
1216 "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
1217 "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
1218 } = response
1219 end
1220
1221 test "returns the favorites of a user" do
1222 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1223 other_user = insert(:user)
1224
1225 {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
1226 {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
1227
1228 {:ok, _, _} = CommonAPI.favorite(activity.id, user)
1229
1230 first_conn = get(conn, "/api/v1/favourites")
1231
1232 assert [status] = json_response(first_conn, 200)
1233 assert status["id"] == to_string(activity.id)
1234
1235 assert [{"link", _link_header}] =
1236 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
1237
1238 # Honours query params
1239 {:ok, second_activity} =
1240 CommonAPI.post(other_user, %{
1241 "status" =>
1242 "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
1243 })
1244
1245 {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
1246
1247 last_like = status["id"]
1248
1249 second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
1250
1251 assert [second_status] = json_response(second_conn, 200)
1252 assert second_status["id"] == to_string(second_activity.id)
1253
1254 third_conn = get(conn, "/api/v1/favourites?limit=0")
1255
1256 assert [] = json_response(third_conn, 200)
1257 end
1258
1259 test "expires_at is nil for another user" do
1260 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1261 {:ok, activity} = CommonAPI.post(user, %{"status" => "foobar", "expires_in" => 1_000_000})
1262
1263 expires_at =
1264 activity.id
1265 |> ActivityExpiration.get_by_activity_id()
1266 |> Map.get(:scheduled_at)
1267 |> NaiveDateTime.to_iso8601()
1268
1269 assert %{"pleroma" => %{"expires_at" => ^expires_at}} =
1270 conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
1271
1272 %{conn: conn} = oauth_access(["read:statuses"])
1273
1274 assert %{"pleroma" => %{"expires_at" => nil}} =
1275 conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
1276 end
1277 end