Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop
[akkoma] / test / web / mastodon_api / views / status_view_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.StatusViewTest do
6 use Pleroma.DataCase
7
8 alias Pleroma.Activity
9 alias Pleroma.Bookmark
10 alias Pleroma.Object
11 alias Pleroma.Repo
12 alias Pleroma.User
13 alias Pleroma.Web.CommonAPI
14 alias Pleroma.Web.CommonAPI.Utils
15 alias Pleroma.Web.MastodonAPI.AccountView
16 alias Pleroma.Web.MastodonAPI.StatusView
17 alias Pleroma.Web.OStatus
18 import Pleroma.Factory
19 import Tesla.Mock
20
21 setup do
22 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
23 :ok
24 end
25
26 test "returns the direct conversation id when given the `with_conversation_id` option" do
27 user = insert(:user)
28
29 {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
30
31 status =
32 StatusView.render("status.json",
33 activity: activity,
34 with_direct_conversation_id: true,
35 for: user
36 )
37
38 assert status[:pleroma][:direct_conversation_id]
39 end
40
41 test "returns a temporary ap_id based user for activities missing db users" do
42 user = insert(:user)
43
44 {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
45
46 Repo.delete(user)
47 Cachex.clear(:user_cache)
48
49 %{account: ms_user} = StatusView.render("status.json", activity: activity)
50
51 assert ms_user.acct == "erroruser@example.com"
52 end
53
54 test "tries to get a user by nickname if fetching by ap_id doesn't work" do
55 user = insert(:user)
56
57 {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
58
59 {:ok, user} =
60 user
61 |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
62 |> Repo.update()
63
64 Cachex.clear(:user_cache)
65
66 result = StatusView.render("status.json", activity: activity)
67
68 assert result[:account][:id] == to_string(user.id)
69 end
70
71 test "a note with null content" do
72 note = insert(:note_activity)
73 note_object = Object.normalize(note)
74
75 data =
76 note_object.data
77 |> Map.put("content", nil)
78
79 Object.change(note_object, %{data: data})
80 |> Object.update_and_set_cache()
81
82 User.get_cached_by_ap_id(note.data["actor"])
83
84 status = StatusView.render("status.json", %{activity: note})
85
86 assert status.content == ""
87 end
88
89 test "a note activity" do
90 note = insert(:note_activity)
91 object_data = Object.normalize(note).data
92 user = User.get_cached_by_ap_id(note.data["actor"])
93
94 convo_id = Utils.context_to_conversation_id(object_data["context"])
95
96 status = StatusView.render("status.json", %{activity: note})
97
98 created_at =
99 (object_data["published"] || "")
100 |> String.replace(~r/\.\d+Z/, ".000Z")
101
102 expected = %{
103 id: to_string(note.id),
104 uri: object_data["id"],
105 url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note),
106 account: AccountView.render("account.json", %{user: user}),
107 in_reply_to_id: nil,
108 in_reply_to_account_id: nil,
109 card: nil,
110 reblog: nil,
111 content: HtmlSanitizeEx.basic_html(object_data["content"]),
112 created_at: created_at,
113 reblogs_count: 0,
114 replies_count: 0,
115 favourites_count: 0,
116 reblogged: false,
117 bookmarked: false,
118 favourited: false,
119 muted: false,
120 pinned: false,
121 sensitive: false,
122 poll: nil,
123 spoiler_text: HtmlSanitizeEx.basic_html(object_data["summary"]),
124 visibility: "public",
125 media_attachments: [],
126 mentions: [],
127 tags: [
128 %{
129 name: "#{object_data["tag"]}",
130 url: "/tag/#{object_data["tag"]}"
131 }
132 ],
133 application: %{
134 name: "Web",
135 website: nil
136 },
137 language: nil,
138 emojis: [
139 %{
140 shortcode: "2hu",
141 url: "corndog.png",
142 static_url: "corndog.png",
143 visible_in_picker: false
144 }
145 ],
146 pleroma: %{
147 local: true,
148 conversation_id: convo_id,
149 in_reply_to_account_acct: nil,
150 content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])},
151 spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])},
152 expires_at: nil,
153 direct_conversation_id: nil,
154 thread_muted: false
155 }
156 }
157
158 assert status == expected
159 end
160
161 test "tells if the message is muted for some reason" do
162 user = insert(:user)
163 other_user = insert(:user)
164
165 {:ok, user} = User.mute(user, other_user)
166
167 {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
168 status = StatusView.render("status.json", %{activity: activity})
169
170 assert status.muted == false
171
172 status = StatusView.render("status.json", %{activity: activity, for: user})
173
174 assert status.muted == true
175 end
176
177 test "tells if the message is thread muted" do
178 user = insert(:user)
179 other_user = insert(:user)
180
181 {:ok, user} = User.mute(user, other_user)
182
183 {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
184 status = StatusView.render("status.json", %{activity: activity, for: user})
185
186 assert status.pleroma.thread_muted == false
187
188 {:ok, activity} = CommonAPI.add_mute(user, activity)
189
190 status = StatusView.render("status.json", %{activity: activity, for: user})
191
192 assert status.pleroma.thread_muted == true
193 end
194
195 test "tells if the status is bookmarked" do
196 user = insert(:user)
197
198 {:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"})
199 status = StatusView.render("status.json", %{activity: activity})
200
201 assert status.bookmarked == false
202
203 status = StatusView.render("status.json", %{activity: activity, for: user})
204
205 assert status.bookmarked == false
206
207 {:ok, _bookmark} = Bookmark.create(user.id, activity.id)
208
209 activity = Activity.get_by_id_with_object(activity.id)
210
211 status = StatusView.render("status.json", %{activity: activity, for: user})
212
213 assert status.bookmarked == true
214 end
215
216 test "a reply" do
217 note = insert(:note_activity)
218 user = insert(:user)
219
220 {:ok, activity} =
221 CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id})
222
223 status = StatusView.render("status.json", %{activity: activity})
224
225 assert status.in_reply_to_id == to_string(note.id)
226
227 [status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
228
229 assert status.in_reply_to_id == to_string(note.id)
230 end
231
232 test "contains mentions" do
233 incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
234 # a user with this ap id might be in the cache.
235 recipient = "https://pleroma.soykaf.com/users/lain"
236 user = insert(:user, %{ap_id: recipient})
237
238 {:ok, [activity]} = OStatus.handle_incoming(incoming)
239
240 status = StatusView.render("status.json", %{activity: activity})
241
242 assert status.mentions ==
243 Enum.map([user], fn u -> AccountView.render("mention.json", %{user: u}) end)
244 end
245
246 test "create mentions from the 'to' field" do
247 %User{ap_id: recipient_ap_id} = insert(:user)
248 cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
249
250 object =
251 insert(:note, %{
252 data: %{
253 "to" => [recipient_ap_id],
254 "cc" => cc
255 }
256 })
257
258 activity =
259 insert(:note_activity, %{
260 note: object,
261 recipients: [recipient_ap_id | cc]
262 })
263
264 assert length(activity.recipients) == 3
265
266 %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity})
267
268 assert length(mentions) == 1
269 assert mention.url == recipient_ap_id
270 end
271
272 test "create mentions from the 'tag' field" do
273 recipient = insert(:user)
274 cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
275
276 object =
277 insert(:note, %{
278 data: %{
279 "cc" => cc,
280 "tag" => [
281 %{
282 "href" => recipient.ap_id,
283 "name" => recipient.nickname,
284 "type" => "Mention"
285 },
286 %{
287 "href" => "https://example.com/search?tag=test",
288 "name" => "#test",
289 "type" => "Hashtag"
290 }
291 ]
292 }
293 })
294
295 activity =
296 insert(:note_activity, %{
297 note: object,
298 recipients: [recipient.ap_id | cc]
299 })
300
301 assert length(activity.recipients) == 3
302
303 %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity})
304
305 assert length(mentions) == 1
306 assert mention.url == recipient.ap_id
307 end
308
309 test "attachments" do
310 object = %{
311 "type" => "Image",
312 "url" => [
313 %{
314 "mediaType" => "image/png",
315 "href" => "someurl"
316 }
317 ],
318 "uuid" => 6
319 }
320
321 expected = %{
322 id: "1638338801",
323 type: "image",
324 url: "someurl",
325 remote_url: "someurl",
326 preview_url: "someurl",
327 text_url: "someurl",
328 description: nil,
329 pleroma: %{mime_type: "image/png"}
330 }
331
332 assert expected == StatusView.render("attachment.json", %{attachment: object})
333
334 # If theres a "id", use that instead of the generated one
335 object = Map.put(object, "id", 2)
336 assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object})
337 end
338
339 test "put the url advertised in the Activity in to the url attribute" do
340 id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
341 [activity] = Activity.search(nil, id)
342
343 status = StatusView.render("status.json", %{activity: activity})
344
345 assert status.uri == id
346 assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/"
347 end
348
349 test "a reblog" do
350 user = insert(:user)
351 activity = insert(:note_activity)
352
353 {:ok, reblog, _} = CommonAPI.repeat(activity.id, user)
354
355 represented = StatusView.render("status.json", %{for: user, activity: reblog})
356
357 assert represented[:id] == to_string(reblog.id)
358 assert represented[:reblog][:id] == to_string(activity.id)
359 assert represented[:emojis] == []
360 end
361
362 test "a peertube video" do
363 user = insert(:user)
364
365 {:ok, object} =
366 Pleroma.Object.Fetcher.fetch_object_from_id(
367 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
368 )
369
370 %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
371
372 represented = StatusView.render("status.json", %{for: user, activity: activity})
373
374 assert represented[:id] == to_string(activity.id)
375 assert length(represented[:media_attachments]) == 1
376 end
377
378 describe "build_tags/1" do
379 test "it returns a a dictionary tags" do
380 object_tags = [
381 "fediverse",
382 "mastodon",
383 "nextcloud",
384 %{
385 "href" => "https://kawen.space/users/lain",
386 "name" => "@lain@kawen.space",
387 "type" => "Mention"
388 }
389 ]
390
391 assert StatusView.build_tags(object_tags) == [
392 %{name: "fediverse", url: "/tag/fediverse"},
393 %{name: "mastodon", url: "/tag/mastodon"},
394 %{name: "nextcloud", url: "/tag/nextcloud"}
395 ]
396 end
397 end
398
399 describe "rich media cards" do
400 test "a rich media card without a site name renders correctly" do
401 page_url = "http://example.com"
402
403 card = %{
404 url: page_url,
405 image: page_url <> "/example.jpg",
406 title: "Example website"
407 }
408
409 %{provider_name: "example.com"} =
410 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
411 end
412
413 test "a rich media card without a site name or image renders correctly" do
414 page_url = "http://example.com"
415
416 card = %{
417 url: page_url,
418 title: "Example website"
419 }
420
421 %{provider_name: "example.com"} =
422 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
423 end
424
425 test "a rich media card without an image renders correctly" do
426 page_url = "http://example.com"
427
428 card = %{
429 url: page_url,
430 site_name: "Example site name",
431 title: "Example website"
432 }
433
434 %{provider_name: "Example site name"} =
435 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
436 end
437
438 test "a rich media card with all relevant data renders correctly" do
439 page_url = "http://example.com"
440
441 card = %{
442 url: page_url,
443 site_name: "Example site name",
444 title: "Example website",
445 image: page_url <> "/example.jpg",
446 description: "Example description"
447 }
448
449 %{provider_name: "Example site name"} =
450 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
451 end
452 end
453
454 describe "poll view" do
455 test "renders a poll" do
456 user = insert(:user)
457
458 {:ok, activity} =
459 CommonAPI.post(user, %{
460 "status" => "Is Tenshi eating a corndog cute?",
461 "poll" => %{
462 "options" => ["absolutely!", "sure", "yes", "why are you even asking?"],
463 "expires_in" => 20
464 }
465 })
466
467 object = Object.normalize(activity)
468
469 expected = %{
470 emojis: [],
471 expired: false,
472 id: to_string(object.id),
473 multiple: false,
474 options: [
475 %{title: "absolutely!", votes_count: 0},
476 %{title: "sure", votes_count: 0},
477 %{title: "yes", votes_count: 0},
478 %{title: "why are you even asking?", votes_count: 0}
479 ],
480 voted: false,
481 votes_count: 0
482 }
483
484 result = StatusView.render("poll.json", %{object: object})
485 expires_at = result.expires_at
486 result = Map.delete(result, :expires_at)
487
488 assert result == expected
489
490 expires_at = NaiveDateTime.from_iso8601!(expires_at)
491 assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20
492 end
493
494 test "detects if it is multiple choice" do
495 user = insert(:user)
496
497 {:ok, activity} =
498 CommonAPI.post(user, %{
499 "status" => "Which Mastodon developer is your favourite?",
500 "poll" => %{
501 "options" => ["Gargron", "Eugen"],
502 "expires_in" => 20,
503 "multiple" => true
504 }
505 })
506
507 object = Object.normalize(activity)
508
509 assert %{multiple: true} = StatusView.render("poll.json", %{object: object})
510 end
511
512 test "detects emoji" do
513 user = insert(:user)
514
515 {:ok, activity} =
516 CommonAPI.post(user, %{
517 "status" => "What's with the smug face?",
518 "poll" => %{
519 "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"],
520 "expires_in" => 20
521 }
522 })
523
524 object = Object.normalize(activity)
525
526 assert %{emojis: [%{shortcode: "blank"}]} =
527 StatusView.render("poll.json", %{object: object})
528 end
529
530 test "detects vote status" do
531 user = insert(:user)
532 other_user = insert(:user)
533
534 {:ok, activity} =
535 CommonAPI.post(user, %{
536 "status" => "Which input devices do you use?",
537 "poll" => %{
538 "options" => ["mouse", "trackball", "trackpoint"],
539 "multiple" => true,
540 "expires_in" => 20
541 }
542 })
543
544 object = Object.normalize(activity)
545
546 {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2])
547
548 result = StatusView.render("poll.json", %{object: object, for: other_user})
549
550 assert result[:voted] == true
551 assert Enum.at(result[:options], 1)[:votes_count] == 1
552 assert Enum.at(result[:options], 2)[:votes_count] == 1
553 end
554
555 test "does not crash on polls with no end date" do
556 object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i")
557 result = StatusView.render("poll.json", %{object: object})
558
559 assert result[:expires_at] == nil
560 assert result[:expired] == false
561 end
562 end
563
564 test "embeds a relationship in the account" do
565 user = insert(:user)
566 other_user = insert(:user)
567
568 {:ok, activity} =
569 CommonAPI.post(user, %{
570 "status" => "drink more water"
571 })
572
573 result = StatusView.render("status.json", %{activity: activity, for: other_user})
574
575 assert result[:account][:pleroma][:relationship] ==
576 AccountView.render("relationship.json", %{user: other_user, target: user})
577 end
578
579 test "embeds a relationship in the account in reposts" do
580 user = insert(:user)
581 other_user = insert(:user)
582
583 {:ok, activity} =
584 CommonAPI.post(user, %{
585 "status" => "˙˙ɐʎns"
586 })
587
588 {:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user)
589
590 result = StatusView.render("status.json", %{activity: activity, for: user})
591
592 assert result[:account][:pleroma][:relationship] ==
593 AccountView.render("relationship.json", %{user: user, target: other_user})
594
595 assert result[:reblog][:account][:pleroma][:relationship] ==
596 AccountView.render("relationship.json", %{user: user, target: user})
597 end
598
599 test "visibility/list" do
600 user = insert(:user)
601
602 {:ok, list} = Pleroma.List.create("foo", user)
603
604 {:ok, activity} =
605 CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
606
607 status = StatusView.render("status.json", activity: activity)
608
609 assert status.visibility == "list"
610 end
611 end