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