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