Add timeline visibility options
[akkoma] / test / pleroma / web / mastodon_api / views / account_view_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
6 use Pleroma.DataCase, async: false
7
8 alias Pleroma.User
9 alias Pleroma.UserRelationship
10 alias Pleroma.Web.CommonAPI
11 alias Pleroma.Web.MastodonAPI.AccountView
12
13 import Pleroma.Factory
14 import Tesla.Mock
15 import Mock
16
17 setup do
18 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
19 :ok
20 end
21
22 test "Represent a user account" do
23 background_image = %{
24 "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}]
25 }
26
27 user =
28 insert(:user, %{
29 ap_id: "https://example.com/users/chikichikibanban",
30 follower_count: 3,
31 note_count: 5,
32 background: background_image,
33 nickname: "shp@shitposter.club",
34 name: ":karjalanpiirakka: shp",
35 bio:
36 "<script src=\"invalid-html\"></script><span>valid html</span>. a<br>b<br/>c<br >d<br />f '&<>\"",
37 inserted_at: ~N[2017-08-15 15:47:06.597036],
38 emoji: %{"karjalanpiirakka" => "/file.png"},
39 raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"",
40 also_known_as: ["https://shitposter.zone/users/shp"],
41 status_ttl_days: 5
42 })
43
44 insert(:instance, %{host: "example.com", nodeinfo: %{version: "2.1"}})
45
46 expected = %{
47 id: to_string(user.id),
48 username: "shp",
49 acct: user.nickname,
50 display_name: user.name,
51 locked: false,
52 created_at: "2017-08-15T15:47:06.000Z",
53 followers_count: 3,
54 following_count: 0,
55 statuses_count: 5,
56 note: "<span>valid html</span>. a<br/>b<br/>c<br/>d<br/>f &#39;&amp;&lt;&gt;&quot;",
57 url: user.ap_id,
58 akkoma: %{
59 instance: %{
60 name: "example.com",
61 nodeinfo: %{
62 "version" => "2.1"
63 },
64 favicon: nil
65 },
66 status_ttl_days: 5
67 },
68 avatar: "http://localhost:4001/images/avi.png",
69 avatar_static: "http://localhost:4001/images/avi.png",
70 header: "http://localhost:4001/images/banner.png",
71 header_static: "http://localhost:4001/images/banner.png",
72 emojis: [
73 %{
74 static_url: "/file.png",
75 url: "/file.png",
76 shortcode: "karjalanpiirakka",
77 visible_in_picker: false
78 }
79 ],
80 fields: [],
81 bot: false,
82 source: %{
83 note: "valid html. a\nb\nc\nd\nf '&<>\"",
84 sensitive: false,
85 pleroma: %{
86 actor_type: "Person",
87 discoverable: true
88 },
89 fields: []
90 },
91 fqn: "shp@shitposter.club",
92 last_status_at: nil,
93 pleroma: %{
94 ap_id: user.ap_id,
95 also_known_as: ["https://shitposter.zone/users/shp"],
96 background_image: "https://example.com/images/asuka_hospital.png",
97 favicon: nil,
98 is_confirmed: true,
99 tags: [],
100 is_admin: false,
101 is_moderator: false,
102 is_suggested: false,
103 hide_favorites: true,
104 hide_followers: false,
105 hide_follows: false,
106 hide_followers_count: false,
107 hide_follows_count: false,
108 relationship: %{},
109 skip_thread_containment: false
110 }
111 }
112
113 assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true})
114 end
115
116 describe "nodeinfo" do
117 setup do
118 [
119 user: insert(:user, ap_id: "https://somewhere.example.com/users/chikichikibanban"),
120 instance:
121 insert(:instance, %{
122 host: "somewhere.example.com",
123 favicon: "https://example.com/favicon.ico"
124 })
125 ]
126 end
127
128 test "is embedded in the account view", %{user: user} do
129 assert %{
130 akkoma: %{
131 instance: %{
132 name: "somewhere.example.com",
133 nodeinfo: %{
134 "version" => "2.0"
135 },
136 favicon: "https://example.com/favicon.ico"
137 }
138 }
139 } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
140 end
141
142 test "uses local nodeinfo for local users" do
143 user = insert(:user)
144
145 assert %{
146 akkoma: %{
147 instance: %{
148 name: "localhost",
149 nodeinfo: %{
150 software: %{
151 name: "akkoma"
152 }
153 }
154 }
155 }
156 } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
157 end
158 end
159
160 describe "favicon" do
161 setup do
162 [
163 user: insert(:user, ap_id: "https://example.com/users/chikichikibanban"),
164 instance:
165 insert(:instance, %{host: "example.com", favicon: "https://example.com/favicon.ico"})
166 ]
167 end
168
169 test "is parsed when :instance_favicons is enabled", %{user: user} do
170 clear_config([:instances_favicons, :enabled], true)
171
172 assert %{
173 pleroma: %{
174 favicon: "https://example.com/favicon.ico"
175 }
176 } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
177 end
178
179 test "is nil when we have no instance", %{user: user} do
180 user = %{user | ap_id: "https://wowee.example.com/users/2"}
181
182 assert %{pleroma: %{favicon: nil}} =
183 AccountView.render("show.json", %{user: user, skip_visibility_check: true})
184 end
185 end
186
187 test "Represent the user account for the account owner" do
188 user = insert(:user)
189
190 notification_settings = %{
191 block_from_strangers: false,
192 hide_notification_contents: false
193 }
194
195 privacy = user.default_scope
196
197 assert %{
198 pleroma: %{notification_settings: ^notification_settings, allow_following_move: true},
199 source: %{privacy: ^privacy}
200 } = AccountView.render("show.json", %{user: user, for: user})
201 end
202
203 test "Represent a Service(bot) account" do
204 user =
205 insert(:user, %{
206 follower_count: 3,
207 note_count: 5,
208 actor_type: "Service",
209 nickname: "shp@shitposter.club",
210 inserted_at: ~N[2017-08-15 15:47:06.597036]
211 })
212
213 expected = %{
214 id: to_string(user.id),
215 username: "shp",
216 acct: user.nickname,
217 display_name: user.name,
218 locked: false,
219 created_at: "2017-08-15T15:47:06.000Z",
220 followers_count: 3,
221 following_count: 0,
222 statuses_count: 5,
223 note: user.bio,
224 url: user.ap_id,
225 avatar: "http://localhost:4001/images/avi.png",
226 avatar_static: "http://localhost:4001/images/avi.png",
227 header: "http://localhost:4001/images/banner.png",
228 header_static: "http://localhost:4001/images/banner.png",
229 emojis: [],
230 fields: [],
231 bot: true,
232 source: %{
233 note: user.bio,
234 sensitive: false,
235 pleroma: %{
236 actor_type: "Service",
237 discoverable: true
238 },
239 fields: []
240 },
241 fqn: "shp@shitposter.club",
242 last_status_at: nil,
243 akkoma: %{
244 instance: %{
245 name: "localhost",
246 favicon: "http://localhost:4001/favicon.png",
247 nodeinfo: %{version: "2.0"}
248 },
249 status_ttl_days: nil
250 },
251 pleroma: %{
252 ap_id: user.ap_id,
253 also_known_as: [],
254 background_image: nil,
255 favicon: "http://localhost:4001/favicon.png",
256 is_confirmed: true,
257 tags: [],
258 is_admin: false,
259 is_moderator: false,
260 is_suggested: false,
261 hide_favorites: true,
262 hide_followers: false,
263 hide_follows: false,
264 hide_followers_count: false,
265 hide_follows_count: false,
266 relationship: %{},
267 skip_thread_containment: false
268 }
269 }
270
271 with_mock(
272 Pleroma.Web.Nodeinfo.Nodeinfo,
273 get_nodeinfo: fn _ -> %{version: "2.0"} end
274 ) do
275 assert expected ==
276 AccountView.render("show.json", %{user: user, skip_visibility_check: true})
277 end
278 end
279
280 test "Represent a Funkwhale channel" do
281 {:ok, user} =
282 User.get_or_fetch_by_ap_id(
283 "https://channels.tests.funkwhale.audio/federation/actors/compositions"
284 )
285
286 assert represented =
287 AccountView.render("show.json", %{user: user, skip_visibility_check: true})
288
289 assert represented.acct == "compositions@channels.tests.funkwhale.audio"
290 assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions"
291 end
292
293 test "Represent a deactivated user for an admin" do
294 admin = insert(:user, is_admin: true)
295 deactivated_user = insert(:user, is_active: false)
296 represented = AccountView.render("show.json", %{user: deactivated_user, for: admin})
297 assert represented[:pleroma][:deactivated] == true
298 end
299
300 test "Represent a smaller mention" do
301 user = insert(:user)
302
303 expected = %{
304 id: to_string(user.id),
305 acct: user.nickname,
306 username: user.nickname,
307 url: user.ap_id
308 }
309
310 assert expected == AccountView.render("mention.json", %{user: user})
311 end
312
313 test "demands :for or :skip_visibility_check option for account rendering" do
314 clear_config([:restrict_unauthenticated, :profiles, :local], false)
315
316 user = insert(:user)
317 user_id = user.id
318
319 assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: nil})
320 assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: user})
321
322 assert %{id: ^user_id} =
323 AccountView.render("show.json", %{user: user, skip_visibility_check: true})
324
325 assert_raise RuntimeError, ~r/:skip_visibility_check or :for option is required/, fn ->
326 AccountView.render("show.json", %{user: user})
327 end
328 end
329
330 describe "relationship" do
331 defp test_relationship_rendering(user, other_user, expected_result) do
332 opts = %{user: user, target: other_user, relationships: nil}
333 assert expected_result == AccountView.render("relationship.json", opts)
334
335 relationships_opt = UserRelationship.view_relationships_option(user, [other_user])
336 opts = Map.put(opts, :relationships, relationships_opt)
337 assert expected_result == AccountView.render("relationship.json", opts)
338
339 assert [expected_result] ==
340 AccountView.render("relationships.json", %{user: user, targets: [other_user]})
341 end
342
343 @blank_response %{
344 following: false,
345 followed_by: false,
346 blocking: false,
347 blocked_by: false,
348 muting: false,
349 muting_notifications: false,
350 subscribing: false,
351 notifying: false,
352 requested: false,
353 requested_by: false,
354 domain_blocking: false,
355 showing_reblogs: true,
356 endorsed: false,
357 note: ""
358 }
359
360 test "represent a relationship for the following and followed user" do
361 user = insert(:user)
362 other_user = insert(:user)
363
364 {:ok, user, other_user} = User.follow(user, other_user)
365 {:ok, other_user, user} = User.follow(other_user, user)
366 {:ok, _subscription} = User.subscribe(user, other_user)
367 {:ok, _user_relationships} = User.mute(user, other_user, %{notifications: true})
368 {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
369
370 expected =
371 Map.merge(
372 @blank_response,
373 %{
374 following: true,
375 followed_by: true,
376 muting: true,
377 muting_notifications: true,
378 subscribing: true,
379 notifying: true,
380 showing_reblogs: false,
381 id: to_string(other_user.id)
382 }
383 )
384
385 test_relationship_rendering(user, other_user, expected)
386 end
387
388 test "represent a relationship for the blocking and blocked user" do
389 user = insert(:user)
390 other_user = insert(:user)
391
392 {:ok, user, other_user} = User.follow(user, other_user)
393 {:ok, _subscription} = User.subscribe(user, other_user)
394 {:ok, _user_relationship} = User.block(user, other_user)
395 {:ok, _user_relationship} = User.block(other_user, user)
396
397 expected =
398 Map.merge(
399 @blank_response,
400 %{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)}
401 )
402
403 test_relationship_rendering(user, other_user, expected)
404 end
405
406 test "represent a relationship for the user blocking a domain" do
407 user = insert(:user)
408 other_user = insert(:user, ap_id: "https://bad.site/users/other_user")
409
410 {:ok, user} = User.block_domain(user, "bad.site")
411
412 expected =
413 Map.merge(
414 @blank_response,
415 %{domain_blocking: true, blocking: false, id: to_string(other_user.id)}
416 )
417
418 test_relationship_rendering(user, other_user, expected)
419 end
420
421 test "represent a relationship for the user with a pending follow request" do
422 user = insert(:user)
423 other_user = insert(:user, is_locked: true)
424
425 {:ok, user, other_user, _} = CommonAPI.follow(user, other_user)
426 user = User.get_cached_by_id(user.id)
427 other_user = User.get_cached_by_id(other_user.id)
428
429 expected =
430 Map.merge(
431 @blank_response,
432 %{requested: true, following: false, id: to_string(other_user.id)}
433 )
434
435 test_relationship_rendering(user, other_user, expected)
436 end
437 end
438
439 test "represent a relationship for a user with an inbound pending follow request" do
440 follower = insert(:user)
441 followed = insert(:user, is_locked: true)
442
443 {:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
444
445 follower = User.get_cached_by_id(follower.id)
446 followed = User.get_cached_by_id(followed.id)
447
448 expected =
449 Map.merge(
450 @blank_response,
451 %{requested_by: true, followed_by: false, id: to_string(follower.id)}
452 )
453
454 test_relationship_rendering(followed, follower, expected)
455 end
456
457 test "returns the settings store if the requesting user is the represented user and it's requested specifically" do
458 user = insert(:user, pleroma_settings_store: %{fe: "test"})
459
460 result =
461 AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true})
462
463 assert result.pleroma.settings_store == %{:fe => "test"}
464
465 result = AccountView.render("show.json", %{user: user, for: nil, with_pleroma_settings: true})
466 assert result.pleroma[:settings_store] == nil
467
468 result = AccountView.render("show.json", %{user: user, for: user})
469 assert result.pleroma[:settings_store] == nil
470 end
471
472 test "doesn't sanitize display names" do
473 user = insert(:user, name: "<marquee> username </marquee>")
474 result = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
475 assert result.display_name == "<marquee> username </marquee>"
476 end
477
478 test "never display nil user follow counts" do
479 user = insert(:user, following_count: 0, follower_count: 0)
480 result = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
481
482 assert result.following_count == 0
483 assert result.followers_count == 0
484 end
485
486 describe "hiding follows/following" do
487 test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do
488 user =
489 insert(:user, %{
490 hide_followers: true,
491 hide_followers_count: true,
492 hide_follows: true,
493 hide_follows_count: true
494 })
495
496 other_user = insert(:user)
497 {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
498 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
499
500 assert %{
501 followers_count: 0,
502 following_count: 0,
503 pleroma: %{hide_follows_count: true, hide_followers_count: true}
504 } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
505 end
506
507 test "shows when follows/followers are hidden" do
508 user = insert(:user, hide_followers: true, hide_follows: true)
509 other_user = insert(:user)
510 {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
511 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
512
513 assert %{
514 followers_count: 1,
515 following_count: 1,
516 pleroma: %{hide_follows: true, hide_followers: true}
517 } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
518 end
519
520 test "shows actual follower/following count to the account owner" do
521 user = insert(:user, hide_followers: true, hide_follows: true)
522 other_user = insert(:user)
523 {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
524
525 assert User.following?(user, other_user)
526 assert Pleroma.FollowingRelationship.follower_count(other_user) == 1
527 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
528
529 assert %{
530 followers_count: 1,
531 following_count: 1
532 } = AccountView.render("show.json", %{user: user, for: user})
533 end
534
535 test "shows unread_conversation_count only to the account owner" do
536 user = insert(:user)
537 other_user = insert(:user)
538
539 {:ok, _activity} =
540 CommonAPI.post(other_user, %{
541 status: "Hey @#{user.nickname}.",
542 visibility: "direct"
543 })
544
545 user = User.get_cached_by_ap_id(user.ap_id)
546
547 assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][
548 :unread_conversation_count
549 ] == nil
550
551 assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][
552 :unread_conversation_count
553 ] == 1
554 end
555
556 test "shows unread_count only to the account owner" do
557 user = insert(:user)
558 insert_list(7, :notification, user: user, activity: insert(:note_activity))
559 other_user = insert(:user)
560
561 user = User.get_cached_by_ap_id(user.ap_id)
562
563 assert AccountView.render(
564 "show.json",
565 %{user: user, for: other_user}
566 )[:pleroma][:unread_notifications_count] == nil
567
568 assert AccountView.render(
569 "show.json",
570 %{user: user, for: user}
571 )[:pleroma][:unread_notifications_count] == 7
572 end
573
574 test "shows email only to the account owner" do
575 user = insert(:user)
576 other_user = insert(:user)
577
578 user = User.get_cached_by_ap_id(user.ap_id)
579
580 assert AccountView.render(
581 "show.json",
582 %{user: user, for: other_user}
583 )[:pleroma][:email] == nil
584
585 assert AccountView.render(
586 "show.json",
587 %{user: user, for: user}
588 )[:pleroma][:email] == user.email
589 end
590 end
591
592 describe "follow requests counter" do
593 test "shows zero when no follow requests are pending" do
594 user = insert(:user)
595
596 assert %{follow_requests_count: 0} =
597 AccountView.render("show.json", %{user: user, for: user})
598
599 other_user = insert(:user)
600 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
601
602 assert %{follow_requests_count: 0} =
603 AccountView.render("show.json", %{user: user, for: user})
604 end
605
606 test "shows non-zero when follow requests are pending" do
607 user = insert(:user, is_locked: true)
608
609 assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
610
611 other_user = insert(:user)
612 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
613
614 assert %{locked: true, follow_requests_count: 1} =
615 AccountView.render("show.json", %{user: user, for: user})
616 end
617
618 test "decreases when accepting a follow request" do
619 user = insert(:user, is_locked: true)
620
621 assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
622
623 other_user = insert(:user)
624 {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user)
625
626 assert %{locked: true, follow_requests_count: 1} =
627 AccountView.render("show.json", %{user: user, for: user})
628
629 {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user)
630
631 assert %{locked: true, follow_requests_count: 0} =
632 AccountView.render("show.json", %{user: user, for: user})
633 end
634
635 test "decreases when rejecting a follow request" do
636 user = insert(:user, is_locked: true)
637
638 assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
639
640 other_user = insert(:user)
641 {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user)
642
643 assert %{locked: true, follow_requests_count: 1} =
644 AccountView.render("show.json", %{user: user, for: user})
645
646 {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user)
647
648 assert %{locked: true, follow_requests_count: 0} =
649 AccountView.render("show.json", %{user: user, for: user})
650 end
651
652 test "shows non-zero when historical unapproved requests are present" do
653 user = insert(:user, is_locked: true)
654
655 assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
656
657 other_user = insert(:user)
658 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
659
660 {:ok, user} = User.update_and_set_cache(user, %{is_locked: false})
661
662 assert %{locked: false, follow_requests_count: 1} =
663 AccountView.render("show.json", %{user: user, for: user})
664 end
665 end
666
667 test "uses mediaproxy urls when it's enabled (regardless of media preview proxy state)" do
668 clear_config([:media_proxy, :enabled], true)
669 clear_config([:media_preview_proxy, :enabled])
670
671 user =
672 insert(:user,
673 avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]},
674 banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]},
675 emoji: %{"joker_smile" => "https://evil.website/society.png"}
676 )
677
678 insert(:instance, %{host: "localhost", favicon: "https://evil.website/favicon.png"})
679
680 with media_preview_enabled <- [false, true] do
681 clear_config([:media_preview_proxy, :enabled], media_preview_enabled)
682
683 AccountView.render("show.json", %{user: user, skip_visibility_check: true})
684 |> Enum.all?(fn
685 {key, url} when key in [:avatar, :avatar_static, :header, :header_static] ->
686 String.starts_with?(url, Pleroma.Web.Endpoint.url())
687
688 {:akkoma, %{instance: %{favicon: favicon_url}}} ->
689 String.starts_with?(favicon_url, Pleroma.Web.Endpoint.url())
690
691 {:emojis, emojis} ->
692 Enum.all?(emojis, fn %{url: url, static_url: static_url} ->
693 String.starts_with?(url, Pleroma.Web.Endpoint.url()) &&
694 String.starts_with?(static_url, Pleroma.Web.Endpoint.url())
695 end)
696
697 _ ->
698 true
699 end)
700 |> assert()
701 end
702 end
703
704 test "returns nil in the instance field when no instance is held locally" do
705 user = insert(:user, ap_id: "https://example.com/users/1")
706 view = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
707 assert view[:akkoma][:instance] == nil
708 end
709 end