Merge branch 'remake-remodel' into develop
[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 "getting a status that doesn't exist returns 404" do
480 %{conn: conn} = oauth_access(["read:statuses"])
481 activity = insert(:note_activity)
482
483 conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
484
485 assert json_response(conn, 404) == %{"error" => "Record not found"}
486 end
487
488 test "get a direct status" do
489 %{user: user, conn: conn} = oauth_access(["read:statuses"])
490 other_user = insert(:user)
491
492 {:ok, activity} =
493 CommonAPI.post(user, %{"status" => "@#{other_user.nickname}", "visibility" => "direct"})
494
495 conn =
496 conn
497 |> assign(:user, user)
498 |> get("/api/v1/statuses/#{activity.id}")
499
500 [participation] = Participation.for_user(user)
501
502 res = json_response(conn, 200)
503 assert res["pleroma"]["direct_conversation_id"] == participation.id
504 end
505
506 test "get statuses by IDs" do
507 %{conn: conn} = oauth_access(["read:statuses"])
508 %{id: id1} = insert(:note_activity)
509 %{id: id2} = insert(:note_activity)
510
511 query_string = "ids[]=#{id1}&ids[]=#{id2}"
512 conn = get(conn, "/api/v1/statuses/?#{query_string}")
513
514 assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"])
515 end
516
517 describe "deleting a status" do
518 test "when you created it" do
519 %{user: author, conn: conn} = oauth_access(["write:statuses"])
520 activity = insert(:note_activity, user: author)
521
522 conn =
523 conn
524 |> assign(:user, author)
525 |> delete("/api/v1/statuses/#{activity.id}")
526
527 assert %{} = json_response(conn, 200)
528
529 refute Activity.get_by_id(activity.id)
530 end
531
532 test "when it doesn't exist" do
533 %{user: author, conn: conn} = oauth_access(["write:statuses"])
534 activity = insert(:note_activity, user: author)
535
536 conn =
537 conn
538 |> assign(:user, author)
539 |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
540
541 assert %{"error" => "Record not found"} == json_response(conn, 404)
542 end
543
544 test "when you didn't create it" do
545 %{conn: conn} = oauth_access(["write:statuses"])
546 activity = insert(:note_activity)
547
548 conn = delete(conn, "/api/v1/statuses/#{activity.id}")
549
550 assert %{"error" => _} = json_response(conn, 403)
551
552 assert Activity.get_by_id(activity.id) == activity
553 end
554
555 test "when you're an admin or moderator", %{conn: conn} do
556 activity1 = insert(:note_activity)
557 activity2 = insert(:note_activity)
558 admin = insert(:user, is_admin: true)
559 moderator = insert(:user, is_moderator: true)
560
561 res_conn =
562 conn
563 |> assign(:user, admin)
564 |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
565 |> delete("/api/v1/statuses/#{activity1.id}")
566
567 assert %{} = json_response(res_conn, 200)
568
569 res_conn =
570 conn
571 |> assign(:user, moderator)
572 |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
573 |> delete("/api/v1/statuses/#{activity2.id}")
574
575 assert %{} = json_response(res_conn, 200)
576
577 refute Activity.get_by_id(activity1.id)
578 refute Activity.get_by_id(activity2.id)
579 end
580 end
581
582 describe "reblogging" do
583 setup do: oauth_access(["write:statuses"])
584
585 test "reblogs and returns the reblogged status", %{conn: conn} do
586 activity = insert(:note_activity)
587
588 conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog")
589
590 assert %{
591 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
592 "reblogged" => true
593 } = json_response(conn, 200)
594
595 assert to_string(activity.id) == id
596 end
597
598 test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
599 activity = insert(:note_activity)
600
601 conn = post(conn, "/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
602
603 assert %{"error" => "Record not found"} = json_response(conn, 404)
604 end
605
606 test "reblogs privately and returns the reblogged status", %{conn: conn} do
607 activity = insert(:note_activity)
608
609 conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
610
611 assert %{
612 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
613 "reblogged" => true,
614 "visibility" => "private"
615 } = json_response(conn, 200)
616
617 assert to_string(activity.id) == id
618 end
619
620 test "reblogged status for another user" do
621 activity = insert(:note_activity)
622 user1 = insert(:user)
623 user2 = insert(:user)
624 user3 = insert(:user)
625 {:ok, _} = CommonAPI.favorite(user2, activity.id)
626 {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
627 {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
628 {:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
629
630 conn_res =
631 build_conn()
632 |> assign(:user, user3)
633 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
634 |> get("/api/v1/statuses/#{reblog_activity1.id}")
635
636 assert %{
637 "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
638 "reblogged" => false,
639 "favourited" => false,
640 "bookmarked" => false
641 } = json_response(conn_res, 200)
642
643 conn_res =
644 build_conn()
645 |> assign(:user, user2)
646 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
647 |> get("/api/v1/statuses/#{reblog_activity1.id}")
648
649 assert %{
650 "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
651 "reblogged" => true,
652 "favourited" => true,
653 "bookmarked" => true
654 } = json_response(conn_res, 200)
655
656 assert to_string(activity.id) == id
657 end
658 end
659
660 describe "unreblogging" do
661 setup do: oauth_access(["write:statuses"])
662
663 test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
664 activity = insert(:note_activity)
665
666 {:ok, _, _} = CommonAPI.repeat(activity.id, user)
667
668 conn = post(conn, "/api/v1/statuses/#{activity.id}/unreblog")
669
670 assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
671
672 assert to_string(activity.id) == id
673 end
674
675 test "returns 404 error when activity does not exist", %{conn: conn} do
676 conn = post(conn, "/api/v1/statuses/foo/unreblog")
677
678 assert json_response(conn, 404) == %{"error" => "Record not found"}
679 end
680 end
681
682 describe "favoriting" do
683 setup do: oauth_access(["write:favourites"])
684
685 test "favs a status and returns it", %{conn: conn} do
686 activity = insert(:note_activity)
687
688 conn = post(conn, "/api/v1/statuses/#{activity.id}/favourite")
689
690 assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
691 json_response(conn, 200)
692
693 assert to_string(activity.id) == id
694 end
695
696 test "favoriting twice will just return 200", %{conn: conn} do
697 activity = insert(:note_activity)
698
699 post(conn, "/api/v1/statuses/#{activity.id}/favourite")
700
701 assert post(conn, "/api/v1/statuses/#{activity.id}/favourite")
702 |> json_response(200)
703 end
704
705 test "returns 404 error for a wrong id", %{conn: conn} do
706 conn =
707 conn
708 |> post("/api/v1/statuses/1/favourite")
709
710 assert json_response(conn, 404) == %{"error" => "Record not found"}
711 end
712 end
713
714 describe "unfavoriting" do
715 setup do: oauth_access(["write:favourites"])
716
717 test "unfavorites a status and returns it", %{user: user, conn: conn} do
718 activity = insert(:note_activity)
719
720 {:ok, _} = CommonAPI.favorite(user, activity.id)
721
722 conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite")
723
724 assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
725 json_response(conn, 200)
726
727 assert to_string(activity.id) == id
728 end
729
730 test "returns 404 error for a wrong id", %{conn: conn} do
731 conn = post(conn, "/api/v1/statuses/1/unfavourite")
732
733 assert json_response(conn, 404) == %{"error" => "Record not found"}
734 end
735 end
736
737 describe "pinned statuses" do
738 setup do: oauth_access(["write:accounts"])
739
740 setup %{user: user} do
741 {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
742
743 %{activity: activity}
744 end
745
746 clear_config([:instance, :max_pinned_statuses]) do
747 Config.put([:instance, :max_pinned_statuses], 1)
748 end
749
750 test "pin status", %{conn: conn, user: user, activity: activity} do
751 id_str = to_string(activity.id)
752
753 assert %{"id" => ^id_str, "pinned" => true} =
754 conn
755 |> post("/api/v1/statuses/#{activity.id}/pin")
756 |> json_response(200)
757
758 assert [%{"id" => ^id_str, "pinned" => true}] =
759 conn
760 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
761 |> json_response(200)
762 end
763
764 test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
765 {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
766
767 conn = post(conn, "/api/v1/statuses/#{dm.id}/pin")
768
769 assert json_response(conn, 400) == %{"error" => "Could not pin"}
770 end
771
772 test "unpin status", %{conn: conn, user: user, activity: activity} do
773 {:ok, _} = CommonAPI.pin(activity.id, user)
774 user = refresh_record(user)
775
776 id_str = to_string(activity.id)
777
778 assert %{"id" => ^id_str, "pinned" => false} =
779 conn
780 |> assign(:user, user)
781 |> post("/api/v1/statuses/#{activity.id}/unpin")
782 |> json_response(200)
783
784 assert [] =
785 conn
786 |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
787 |> json_response(200)
788 end
789
790 test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
791 conn = post(conn, "/api/v1/statuses/1/unpin")
792
793 assert json_response(conn, 400) == %{"error" => "Could not unpin"}
794 end
795
796 test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
797 {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
798
799 id_str_one = to_string(activity_one.id)
800
801 assert %{"id" => ^id_str_one, "pinned" => true} =
802 conn
803 |> post("/api/v1/statuses/#{id_str_one}/pin")
804 |> json_response(200)
805
806 user = refresh_record(user)
807
808 assert %{"error" => "You have already pinned the maximum number of statuses"} =
809 conn
810 |> assign(:user, user)
811 |> post("/api/v1/statuses/#{activity_two.id}/pin")
812 |> json_response(400)
813 end
814 end
815
816 describe "cards" do
817 setup do
818 Config.put([:rich_media, :enabled], true)
819
820 oauth_access(["read:statuses"])
821 end
822
823 test "returns rich-media card", %{conn: conn, user: user} do
824 Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
825
826 {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"})
827
828 card_data = %{
829 "image" => "http://ia.media-imdb.com/images/rock.jpg",
830 "provider_name" => "example.com",
831 "provider_url" => "https://example.com",
832 "title" => "The Rock",
833 "type" => "link",
834 "url" => "https://example.com/ogp",
835 "description" =>
836 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
837 "pleroma" => %{
838 "opengraph" => %{
839 "image" => "http://ia.media-imdb.com/images/rock.jpg",
840 "title" => "The Rock",
841 "type" => "video.movie",
842 "url" => "https://example.com/ogp",
843 "description" =>
844 "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
845 }
846 }
847 }
848
849 response =
850 conn
851 |> get("/api/v1/statuses/#{activity.id}/card")
852 |> json_response(200)
853
854 assert response == card_data
855
856 # works with private posts
857 {:ok, activity} =
858 CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"})
859
860 response_two =
861 conn
862 |> get("/api/v1/statuses/#{activity.id}/card")
863 |> json_response(200)
864
865 assert response_two == card_data
866 end
867
868 test "replaces missing description with an empty string", %{conn: conn, user: user} do
869 Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
870
871 {:ok, activity} =
872 CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"})
873
874 response =
875 conn
876 |> get("/api/v1/statuses/#{activity.id}/card")
877 |> json_response(:ok)
878
879 assert response == %{
880 "type" => "link",
881 "title" => "Pleroma",
882 "description" => "",
883 "image" => nil,
884 "provider_name" => "example.com",
885 "provider_url" => "https://example.com",
886 "url" => "https://example.com/ogp-missing-data",
887 "pleroma" => %{
888 "opengraph" => %{
889 "title" => "Pleroma",
890 "type" => "website",
891 "url" => "https://example.com/ogp-missing-data"
892 }
893 }
894 }
895 end
896 end
897
898 test "bookmarks" do
899 %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
900 author = insert(:user)
901
902 {:ok, activity1} =
903 CommonAPI.post(author, %{
904 "status" => "heweoo?"
905 })
906
907 {:ok, activity2} =
908 CommonAPI.post(author, %{
909 "status" => "heweoo!"
910 })
911
912 response1 = post(conn, "/api/v1/statuses/#{activity1.id}/bookmark")
913
914 assert json_response(response1, 200)["bookmarked"] == true
915
916 response2 = post(conn, "/api/v1/statuses/#{activity2.id}/bookmark")
917
918 assert json_response(response2, 200)["bookmarked"] == true
919
920 bookmarks = get(conn, "/api/v1/bookmarks")
921
922 assert [json_response(response2, 200), json_response(response1, 200)] ==
923 json_response(bookmarks, 200)
924
925 response1 = post(conn, "/api/v1/statuses/#{activity1.id}/unbookmark")
926
927 assert json_response(response1, 200)["bookmarked"] == false
928
929 bookmarks = get(conn, "/api/v1/bookmarks")
930
931 assert [json_response(response2, 200)] == json_response(bookmarks, 200)
932 end
933
934 describe "conversation muting" do
935 setup do: oauth_access(["write:mutes"])
936
937 setup do
938 post_user = insert(:user)
939 {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"})
940 %{activity: activity}
941 end
942
943 test "mute conversation", %{conn: conn, activity: activity} do
944 id_str = to_string(activity.id)
945
946 assert %{"id" => ^id_str, "muted" => true} =
947 conn
948 |> post("/api/v1/statuses/#{activity.id}/mute")
949 |> json_response(200)
950 end
951
952 test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
953 {:ok, _} = CommonAPI.add_mute(user, activity)
954
955 conn = post(conn, "/api/v1/statuses/#{activity.id}/mute")
956
957 assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
958 end
959
960 test "unmute conversation", %{conn: conn, user: user, activity: activity} do
961 {:ok, _} = CommonAPI.add_mute(user, activity)
962
963 id_str = to_string(activity.id)
964
965 assert %{"id" => ^id_str, "muted" => false} =
966 conn
967 # |> assign(:user, user)
968 |> post("/api/v1/statuses/#{activity.id}/unmute")
969 |> json_response(200)
970 end
971 end
972
973 test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
974 user1 = insert(:user)
975 user2 = insert(:user)
976 user3 = insert(:user)
977
978 {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
979
980 # Reply to status from another user
981 conn1 =
982 conn
983 |> assign(:user, user2)
984 |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
985 |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
986
987 assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
988
989 activity = Activity.get_by_id_with_object(id)
990
991 assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
992 assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
993
994 # Reblog from the third user
995 conn2 =
996 conn
997 |> assign(:user, user3)
998 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
999 |> post("/api/v1/statuses/#{activity.id}/reblog")
1000
1001 assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
1002 json_response(conn2, 200)
1003
1004 assert to_string(activity.id) == id
1005
1006 # Getting third user status
1007 conn3 =
1008 conn
1009 |> assign(:user, user3)
1010 |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
1011 |> get("api/v1/timelines/home")
1012
1013 [reblogged_activity] = json_response(conn3, 200)
1014
1015 assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
1016
1017 replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
1018 assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
1019 end
1020
1021 describe "GET /api/v1/statuses/:id/favourited_by" do
1022 setup do: oauth_access(["read:accounts"])
1023
1024 setup %{user: user} do
1025 {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
1026
1027 %{activity: activity}
1028 end
1029
1030 test "returns users who have favorited the status", %{conn: conn, activity: activity} do
1031 other_user = insert(:user)
1032 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1033
1034 response =
1035 conn
1036 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1037 |> json_response(:ok)
1038
1039 [%{"id" => id}] = response
1040
1041 assert id == other_user.id
1042 end
1043
1044 test "returns empty array when status has not been favorited yet", %{
1045 conn: conn,
1046 activity: activity
1047 } do
1048 response =
1049 conn
1050 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1051 |> json_response(:ok)
1052
1053 assert Enum.empty?(response)
1054 end
1055
1056 test "does not return users who have favorited the status but are blocked", %{
1057 conn: %{assigns: %{user: user}} = conn,
1058 activity: activity
1059 } do
1060 other_user = insert(:user)
1061 {:ok, _user_relationship} = User.block(user, other_user)
1062
1063 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1064
1065 response =
1066 conn
1067 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1068 |> json_response(:ok)
1069
1070 assert Enum.empty?(response)
1071 end
1072
1073 test "does not fail on an unauthenticated request", %{activity: activity} do
1074 other_user = insert(:user)
1075 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1076
1077 response =
1078 build_conn()
1079 |> get("/api/v1/statuses/#{activity.id}/favourited_by")
1080 |> json_response(:ok)
1081
1082 [%{"id" => id}] = response
1083 assert id == other_user.id
1084 end
1085
1086 test "requires authentication for private posts", %{user: user} do
1087 other_user = insert(:user)
1088
1089 {:ok, activity} =
1090 CommonAPI.post(user, %{
1091 "status" => "@#{other_user.nickname} wanna get some #cofe together?",
1092 "visibility" => "direct"
1093 })
1094
1095 {:ok, _} = CommonAPI.favorite(other_user, activity.id)
1096
1097 favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
1098
1099 build_conn()
1100 |> get(favourited_by_url)
1101 |> json_response(404)
1102
1103 conn =
1104 build_conn()
1105 |> assign(:user, other_user)
1106 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1107
1108 conn
1109 |> assign(:token, nil)
1110 |> get(favourited_by_url)
1111 |> json_response(404)
1112
1113 response =
1114 conn
1115 |> get(favourited_by_url)
1116 |> json_response(200)
1117
1118 [%{"id" => id}] = response
1119 assert id == other_user.id
1120 end
1121 end
1122
1123 describe "GET /api/v1/statuses/:id/reblogged_by" do
1124 setup do: oauth_access(["read:accounts"])
1125
1126 setup %{user: user} do
1127 {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
1128
1129 %{activity: activity}
1130 end
1131
1132 test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
1133 other_user = insert(:user)
1134 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1135
1136 response =
1137 conn
1138 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1139 |> json_response(:ok)
1140
1141 [%{"id" => id}] = response
1142
1143 assert id == other_user.id
1144 end
1145
1146 test "returns empty array when status has not been reblogged yet", %{
1147 conn: conn,
1148 activity: activity
1149 } do
1150 response =
1151 conn
1152 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1153 |> json_response(:ok)
1154
1155 assert Enum.empty?(response)
1156 end
1157
1158 test "does not return users who have reblogged the status but are blocked", %{
1159 conn: %{assigns: %{user: user}} = conn,
1160 activity: activity
1161 } do
1162 other_user = insert(:user)
1163 {:ok, _user_relationship} = User.block(user, other_user)
1164
1165 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1166
1167 response =
1168 conn
1169 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1170 |> json_response(:ok)
1171
1172 assert Enum.empty?(response)
1173 end
1174
1175 test "does not return users who have reblogged the status privately", %{
1176 conn: conn,
1177 activity: activity
1178 } do
1179 other_user = insert(:user)
1180
1181 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user, %{"visibility" => "private"})
1182
1183 response =
1184 conn
1185 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1186 |> json_response(:ok)
1187
1188 assert Enum.empty?(response)
1189 end
1190
1191 test "does not fail on an unauthenticated request", %{activity: activity} do
1192 other_user = insert(:user)
1193 {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
1194
1195 response =
1196 build_conn()
1197 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1198 |> json_response(:ok)
1199
1200 [%{"id" => id}] = response
1201 assert id == other_user.id
1202 end
1203
1204 test "requires authentication for private posts", %{user: user} do
1205 other_user = insert(:user)
1206
1207 {:ok, activity} =
1208 CommonAPI.post(user, %{
1209 "status" => "@#{other_user.nickname} wanna get some #cofe together?",
1210 "visibility" => "direct"
1211 })
1212
1213 build_conn()
1214 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1215 |> json_response(404)
1216
1217 response =
1218 build_conn()
1219 |> assign(:user, other_user)
1220 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
1221 |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
1222 |> json_response(200)
1223
1224 assert [] == response
1225 end
1226 end
1227
1228 test "context" do
1229 user = insert(:user)
1230
1231 {:ok, %{id: id1}} = CommonAPI.post(user, %{"status" => "1"})
1232 {:ok, %{id: id2}} = CommonAPI.post(user, %{"status" => "2", "in_reply_to_status_id" => id1})
1233 {:ok, %{id: id3}} = CommonAPI.post(user, %{"status" => "3", "in_reply_to_status_id" => id2})
1234 {:ok, %{id: id4}} = CommonAPI.post(user, %{"status" => "4", "in_reply_to_status_id" => id3})
1235 {:ok, %{id: id5}} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4})
1236
1237 response =
1238 build_conn()
1239 |> get("/api/v1/statuses/#{id3}/context")
1240 |> json_response(:ok)
1241
1242 assert %{
1243 "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
1244 "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
1245 } = response
1246 end
1247
1248 test "returns the favorites of a user" do
1249 %{user: user, conn: conn} = oauth_access(["read:favourites"])
1250 other_user = insert(:user)
1251
1252 {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
1253 {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
1254
1255 {:ok, _} = CommonAPI.favorite(user, activity.id)
1256
1257 first_conn = get(conn, "/api/v1/favourites")
1258
1259 assert [status] = json_response(first_conn, 200)
1260 assert status["id"] == to_string(activity.id)
1261
1262 assert [{"link", _link_header}] =
1263 Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
1264
1265 # Honours query params
1266 {:ok, second_activity} =
1267 CommonAPI.post(other_user, %{
1268 "status" =>
1269 "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
1270 })
1271
1272 {:ok, _} = CommonAPI.favorite(user, second_activity.id)
1273
1274 last_like = status["id"]
1275
1276 second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
1277
1278 assert [second_status] = json_response(second_conn, 200)
1279 assert second_status["id"] == to_string(second_activity.id)
1280
1281 third_conn = get(conn, "/api/v1/favourites?limit=0")
1282
1283 assert [] = json_response(third_conn, 200)
1284 end
1285
1286 test "expires_at is nil for another user" do
1287 %{conn: conn, user: user} = oauth_access(["read:statuses"])
1288 {:ok, activity} = CommonAPI.post(user, %{"status" => "foobar", "expires_in" => 1_000_000})
1289
1290 expires_at =
1291 activity.id
1292 |> ActivityExpiration.get_by_activity_id()
1293 |> Map.get(:scheduled_at)
1294 |> NaiveDateTime.to_iso8601()
1295
1296 assert %{"pleroma" => %{"expires_at" => ^expires_at}} =
1297 conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
1298
1299 %{conn: conn} = oauth_access(["read:statuses"])
1300
1301 assert %{"pleroma" => %{"expires_at" => nil}} =
1302 conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
1303 end
1304 end