# Pleroma: A lightweight social networking server # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.AccountViewTest do use Pleroma.DataCase, async: false alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView import Pleroma.Factory import Tesla.Mock import Mock setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok end test "Represent a user account" do background_image = %{ "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] } user = insert(:user, %{ ap_id: "https://example.com/users/chikichikibanban", follower_count: 3, note_count: 5, background: background_image, nickname: "shp@shitposter.club", name: ":karjalanpiirakka: shp", bio: "valid html. a
b
c
d
f '&<>\"", inserted_at: ~N[2017-08-15 15:47:06.597036], emoji: %{"karjalanpiirakka" => "/file.png"}, raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"", also_known_as: ["https://shitposter.zone/users/shp"], status_ttl_days: 5 }) insert(:instance, %{host: "example.com", nodeinfo: %{version: "2.1"}}) expected = %{ id: to_string(user.id), username: "shp", acct: user.nickname, display_name: user.name, locked: false, created_at: "2017-08-15T15:47:06.000Z", followers_count: 3, following_count: 0, statuses_count: 5, note: "valid html. a
b
c
d
f '&<>"", url: user.ap_id, akkoma: %{ instance: %{ name: "example.com", nodeinfo: %{ "version" => "2.1" }, favicon: nil }, status_ttl_days: 5 }, avatar: "http://localhost:4001/images/avi.png", avatar_static: "http://localhost:4001/images/avi.png", header: "http://localhost:4001/images/banner.png", header_static: "http://localhost:4001/images/banner.png", emojis: [ %{ static_url: "/file.png", url: "/file.png", shortcode: "karjalanpiirakka", visible_in_picker: false } ], fields: [], bot: false, source: %{ note: "valid html. a\nb\nc\nd\nf '&<>\"", sensitive: false, pleroma: %{ actor_type: "Person", discoverable: true }, fields: [] }, fqn: "shp@shitposter.club", last_status_at: nil, pleroma: %{ ap_id: user.ap_id, also_known_as: ["https://shitposter.zone/users/shp"], background_image: "https://example.com/images/asuka_hospital.png", favicon: nil, is_confirmed: true, tags: [], is_admin: false, is_moderator: false, is_suggested: false, hide_favorites: true, hide_followers: false, hide_follows: false, hide_followers_count: false, hide_follows_count: false, relationship: %{}, skip_thread_containment: false } } assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end describe "nodeinfo" do setup do [ user: insert(:user, ap_id: "https://somewhere.example.com/users/chikichikibanban"), instance: insert(:instance, %{ host: "somewhere.example.com", favicon: "https://example.com/favicon.ico" }) ] end test "is embedded in the account view", %{user: user} do assert %{ akkoma: %{ instance: %{ name: "somewhere.example.com", nodeinfo: %{ "version" => "2.0" }, favicon: "https://example.com/favicon.ico" } } } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end test "uses local nodeinfo for local users" do user = insert(:user) assert %{ akkoma: %{ instance: %{ name: "localhost", nodeinfo: %{ software: %{ name: "akkoma" } } } } } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end end describe "favicon" do setup do [ user: insert(:user, ap_id: "https://example.com/users/chikichikibanban"), instance: insert(:instance, %{host: "example.com", favicon: "https://example.com/favicon.ico"}) ] end test "is parsed when :instance_favicons is enabled", %{user: user} do clear_config([:instances_favicons, :enabled], true) assert %{ pleroma: %{ favicon: "https://example.com/favicon.ico" } } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end test "is nil when we have no instance", %{user: user} do user = %{user | ap_id: "https://wowee.example.com/users/2"} assert %{pleroma: %{favicon: nil}} = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end end test "Represent the user account for the account owner" do user = insert(:user) notification_settings = %{ block_from_strangers: false, hide_notification_contents: false } privacy = user.default_scope assert %{ pleroma: %{notification_settings: ^notification_settings, allow_following_move: true}, source: %{privacy: ^privacy} } = AccountView.render("show.json", %{user: user, for: user}) end test "Represent a Service(bot) account" do user = insert(:user, %{ follower_count: 3, note_count: 5, actor_type: "Service", nickname: "shp@shitposter.club", inserted_at: ~N[2017-08-15 15:47:06.597036] }) expected = %{ id: to_string(user.id), username: "shp", acct: user.nickname, display_name: user.name, locked: false, created_at: "2017-08-15T15:47:06.000Z", followers_count: 3, following_count: 0, statuses_count: 5, note: user.bio, url: user.ap_id, avatar: "http://localhost:4001/images/avi.png", avatar_static: "http://localhost:4001/images/avi.png", header: "http://localhost:4001/images/banner.png", header_static: "http://localhost:4001/images/banner.png", emojis: [], fields: [], bot: true, source: %{ note: user.bio, sensitive: false, pleroma: %{ actor_type: "Service", discoverable: true }, fields: [] }, fqn: "shp@shitposter.club", last_status_at: nil, akkoma: %{ instance: %{ name: "localhost", favicon: "http://localhost:4001/favicon.png", nodeinfo: %{version: "2.0"} }, status_ttl_days: nil }, pleroma: %{ ap_id: user.ap_id, also_known_as: [], background_image: nil, favicon: "http://localhost:4001/favicon.png", is_confirmed: true, tags: [], is_admin: false, is_moderator: false, is_suggested: false, hide_favorites: true, hide_followers: false, hide_follows: false, hide_followers_count: false, hide_follows_count: false, relationship: %{}, skip_thread_containment: false } } with_mock( Pleroma.Web.Nodeinfo.Nodeinfo, get_nodeinfo: fn _ -> %{version: "2.0"} end ) do assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end end test "Represent a Funkwhale channel" do {:ok, user} = User.get_or_fetch_by_ap_id( "https://channels.tests.funkwhale.audio/federation/actors/compositions" ) assert represented = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) assert represented.acct == "compositions@channels.tests.funkwhale.audio" assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" end test "Represent a deactivated user for an admin" do admin = insert(:user, is_admin: true) deactivated_user = insert(:user, is_active: false) represented = AccountView.render("show.json", %{user: deactivated_user, for: admin}) assert represented[:pleroma][:deactivated] == true end test "Represent a smaller mention" do user = insert(:user) expected = %{ id: to_string(user.id), acct: user.nickname, username: user.nickname, url: user.ap_id } assert expected == AccountView.render("mention.json", %{user: user}) end test "demands :for or :skip_visibility_check option for account rendering" do clear_config([:restrict_unauthenticated, :profiles, :local], false) user = insert(:user) user_id = user.id assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: nil}) assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: user}) assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) assert_raise RuntimeError, ~r/:skip_visibility_check or :for option is required/, fn -> AccountView.render("show.json", %{user: user}) end end describe "relationship" do defp test_relationship_rendering(user, other_user, expected_result) do opts = %{user: user, target: other_user, relationships: nil} assert expected_result == AccountView.render("relationship.json", opts) relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) opts = Map.put(opts, :relationships, relationships_opt) assert expected_result == AccountView.render("relationship.json", opts) assert [expected_result] == AccountView.render("relationships.json", %{user: user, targets: [other_user]}) end @blank_response %{ following: false, followed_by: false, blocking: false, blocked_by: false, muting: false, muting_notifications: false, subscribing: false, notifying: false, requested: false, requested_by: false, domain_blocking: false, showing_reblogs: true, endorsed: false, note: "" } test "represent a relationship for the following and followed user" do user = insert(:user) other_user = insert(:user) {:ok, user, other_user} = User.follow(user, other_user) {:ok, other_user, user} = User.follow(other_user, user) {:ok, _subscription} = User.subscribe(user, other_user) {:ok, _user_relationships} = User.mute(user, other_user, %{notifications: true}) {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) expected = Map.merge( @blank_response, %{ following: true, followed_by: true, muting: true, muting_notifications: true, subscribing: true, notifying: true, showing_reblogs: false, id: to_string(other_user.id) } ) test_relationship_rendering(user, other_user, expected) end test "represent a relationship for the blocking and blocked user" do user = insert(:user) other_user = insert(:user) {:ok, user, other_user} = User.follow(user, other_user) {:ok, _subscription} = User.subscribe(user, other_user) {:ok, _user_relationship} = User.block(user, other_user) {:ok, _user_relationship} = User.block(other_user, user) expected = Map.merge( @blank_response, %{following: false, blocking: true, blocked_by: false, id: to_string(other_user.id)} ) test_relationship_rendering(user, other_user, expected) end test "blocks are not visible to the blocked user" do user = insert(:user) other_user = insert(:user) {:ok, _user_relationship} = User.block(other_user, user) expected = Map.merge( @blank_response, %{following: false, blocking: false, blocked_by: false, id: to_string(other_user.id)} ) test_relationship_rendering(user, other_user, expected) end test "represent a relationship for the user blocking a domain" do user = insert(:user) other_user = insert(:user, ap_id: "https://bad.site/users/other_user") {:ok, user} = User.block_domain(user, "bad.site") expected = Map.merge( @blank_response, %{domain_blocking: true, blocking: false, id: to_string(other_user.id)} ) test_relationship_rendering(user, other_user, expected) end test "represent a relationship for the user with a pending follow request" do user = insert(:user) other_user = insert(:user, is_locked: true) {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) user = User.get_cached_by_id(user.id) other_user = User.get_cached_by_id(other_user.id) expected = Map.merge( @blank_response, %{requested: true, following: false, id: to_string(other_user.id)} ) test_relationship_rendering(user, other_user, expected) end end test "represent a relationship for a user with an inbound pending follow request" do follower = insert(:user) followed = insert(:user, is_locked: true) {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) follower = User.get_cached_by_id(follower.id) followed = User.get_cached_by_id(followed.id) expected = Map.merge( @blank_response, %{requested_by: true, followed_by: false, id: to_string(follower.id)} ) test_relationship_rendering(followed, follower, expected) end test "returns the settings store if the requesting user is the represented user and it's requested specifically" do user = insert(:user, pleroma_settings_store: %{fe: "test"}) result = AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true}) assert result.pleroma.settings_store == %{:fe => "test"} result = AccountView.render("show.json", %{user: user, for: nil, with_pleroma_settings: true}) assert result.pleroma[:settings_store] == nil result = AccountView.render("show.json", %{user: user, for: user}) assert result.pleroma[:settings_store] == nil end test "doesn't sanitize display names" do user = insert(:user, name: " username ") result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) assert result.display_name == " username " end test "never display nil user follow counts" do user = insert(:user, following_count: 0, follower_count: 0) result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) assert result.following_count == 0 assert result.followers_count == 0 end describe "hiding follows/following" do test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do user = insert(:user, %{ hide_followers: true, hide_followers_count: true, hide_follows: true, hide_follows_count: true }) other_user = insert(:user) {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{ followers_count: 0, following_count: 0, pleroma: %{hide_follows_count: true, hide_followers_count: true} } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end test "shows when follows/followers are hidden" do user = insert(:user, hide_followers: true, hide_follows: true) other_user = insert(:user) {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{ followers_count: 1, following_count: 1, pleroma: %{hide_follows: true, hide_followers: true} } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end test "shows actual follower/following count to the account owner" do user = insert(:user, hide_followers: true, hide_follows: true) other_user = insert(:user) {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) assert User.following?(user, other_user) assert Pleroma.FollowingRelationship.follower_count(other_user) == 1 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{ followers_count: 1, following_count: 1 } = AccountView.render("show.json", %{user: user, for: user}) end test "shows unread_conversation_count only to the account owner" do user = insert(:user) other_user = insert(:user) {:ok, _activity} = CommonAPI.post(other_user, %{ status: "Hey @#{user.nickname}.", visibility: "direct" }) user = User.get_cached_by_ap_id(user.ap_id) assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][ :unread_conversation_count ] == nil assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][ :unread_conversation_count ] == 1 end test "shows unread_count only to the account owner" do user = insert(:user) insert_list(7, :notification, user: user, activity: insert(:note_activity)) other_user = insert(:user) user = User.get_cached_by_ap_id(user.ap_id) assert AccountView.render( "show.json", %{user: user, for: other_user} )[:pleroma][:unread_notifications_count] == nil assert AccountView.render( "show.json", %{user: user, for: user} )[:pleroma][:unread_notifications_count] == 7 end test "shows email only to the account owner" do user = insert(:user) other_user = insert(:user) user = User.get_cached_by_ap_id(user.ap_id) assert AccountView.render( "show.json", %{user: user, for: other_user} )[:pleroma][:email] == nil assert AccountView.render( "show.json", %{user: user, for: user} )[:pleroma][:email] == user.email end end describe "follow requests counter" do test "shows zero when no follow requests are pending" do user = insert(:user) assert %{follow_requests_count: 0} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{follow_requests_count: 0} = AccountView.render("show.json", %{user: user, for: user}) end test "shows non-zero when follow requests are pending" do user = insert(:user, is_locked: true) assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) end test "decreases when accepting a follow request" do user = insert(:user, is_locked: true) assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user) assert %{locked: true, follow_requests_count: 0} = AccountView.render("show.json", %{user: user, for: user}) end test "decreases when rejecting a follow request" do user = insert(:user, is_locked: true) assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user) assert %{locked: true, follow_requests_count: 0} = AccountView.render("show.json", %{user: user, for: user}) end test "shows non-zero when historical unapproved requests are present" do user = insert(:user, is_locked: true) assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) {:ok, user} = User.update_and_set_cache(user, %{is_locked: false}) assert %{locked: false, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) end end test "uses mediaproxy urls when it's enabled (regardless of media preview proxy state)" do clear_config([:media_proxy, :enabled], true) clear_config([:media_preview_proxy, :enabled]) user = insert(:user, avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]}, banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]}, emoji: %{"joker_smile" => "https://evil.website/society.png"} ) insert(:instance, %{host: "localhost", favicon: "https://evil.website/favicon.png"}) with media_preview_enabled <- [false, true] do clear_config([:media_preview_proxy, :enabled], media_preview_enabled) AccountView.render("show.json", %{user: user, skip_visibility_check: true}) |> Enum.all?(fn {key, url} when key in [:avatar, :avatar_static, :header, :header_static] -> String.starts_with?(url, Pleroma.Web.Endpoint.url()) {:akkoma, %{instance: %{favicon: favicon_url}}} -> String.starts_with?(favicon_url, Pleroma.Web.Endpoint.url()) {:emojis, emojis} -> Enum.all?(emojis, fn %{url: url, static_url: static_url} -> String.starts_with?(url, Pleroma.Web.Endpoint.url()) && String.starts_with?(static_url, Pleroma.Web.Endpoint.url()) end) _ -> true end) |> assert() end end test "returns nil in the instance field when no instance is held locally" do user = insert(:user, ap_id: "https://example.com/users/1") view = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) assert view[:akkoma][:instance] == nil end end