Merge branch 'develop' into gitlab-mr-iid-4381

This commit is contained in:
Phantasm 2026-06-30 00:31:51 +02:00
commit 70c79ad512
No known key found for this signature in database
GPG key ID: 2669E588BCC634C8
41 changed files with 2753 additions and 101 deletions

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
alias Pleroma.Integration.WebsocketClient
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth
@moduletag needs_streamer: true, capture_log: true
@ -31,6 +32,48 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
WebsocketClient.start_link(self(), path, headers)
end
defp raw_websocket_handshake(qs, headers) do
uri = URI.parse(@path <> qs)
port = uri.port || 80
path = uri.path <> if(uri.query, do: "?" <> uri.query, else: "")
default_headers = [
{"host", "#{uri.host}:#{port}"},
{"upgrade", "websocket"},
{"connection", "Upgrade"},
{"sec-websocket-key", Base.encode64(:crypto.strong_rand_bytes(16))},
{"sec-websocket-version", "13"}
]
request = [
"GET #{path} HTTP/1.1\r\n",
Enum.map(default_headers ++ headers, fn {name, value} -> "#{name}: #{value}\r\n" end),
"\r\n"
]
with {:ok, socket} <-
:gen_tcp.connect(String.to_charlist(uri.host), port, [:binary, active: false], 1_000),
:ok <- :gen_tcp.send(socket, request),
{:ok, response} <- :gen_tcp.recv(socket, 0, 1_000) do
:gen_tcp.close(socket)
{:ok, parse_http_response(response)}
end
end
defp parse_http_response(response) do
[headers | _] = String.split(response, "\r\n\r\n", parts: 2)
[status_line | header_lines] = String.split(headers, "\r\n")
[_, status | _] = String.split(status_line, " ")
headers =
Enum.map(header_lines, fn line ->
[name, value] = String.split(line, ":", parts: 2)
{String.downcase(name), String.trim(value)}
end)
%{status: String.to_integer(status), headers: headers}
end
defp decode_json(json) do
with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json),
{:ok, payload} <- Jason.decode(payload_text) do
@ -85,9 +128,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
assert json["payload"]
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|> atom_key_to_string()
view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil))
assert json == view_json
end
@ -114,10 +155,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
assert json["payload"]
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|> Jason.encode!()
|> Jason.decode!()
view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil))
assert json == view_json
end
@ -279,6 +317,34 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
end)
end
test "echoes the Sec-WebSocket-Protocol token in the handshake", %{token: token} do
assert {:ok, %{status: 101, headers: headers}} =
raw_websocket_handshake("?stream=user", [
{"sec-websocket-protocol", token.token}
])
assert {"sec-websocket-protocol", token.token} in headers
end
test "echoes the selected Sec-WebSocket-Protocol token", %{token: token} do
assert {:ok, %{status: 101, headers: headers}} =
raw_websocket_handshake("?stream=user", [
{"sec-websocket-protocol", "#{token.token}, phoenix"}
])
assert {"sec-websocket-protocol", token.token} in headers
end
test "does not echo an invalid Sec-WebSocket-Protocol token", %{token: token} do
assert {:ok, %{status: 401, headers: headers}} =
raw_websocket_handshake("?stream=user", [
{"sec-websocket-protocol", "invalid"}
])
refute {"sec-websocket-protocol", token.token} in headers
refute List.keymember?(headers, "sec-websocket-protocol", 0)
end
test "prefers sec-websocket-protocol token over query access_token", %{
token: token,
user: user
@ -450,12 +516,12 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json",
activity: activity,
for: reading_user
atom_key_to_string(
StatusView.render("show.json",
activity: activity,
for: reading_user
)
)
|> Jason.encode!()
|> Jason.decode!()
assert json == view_json
end
@ -478,12 +544,12 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
activity = Pleroma.Activity.normalize(activity)
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json",
activity: activity,
for: reading_user
atom_key_to_string(
StatusView.render("show.json",
activity: activity,
for: reading_user
)
)
|> Jason.encode!()
|> Jason.decode!()
assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json)
end

View file

@ -415,6 +415,105 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
end
test "fetches user featured collection from the first collection page" do
ap_id = "https://example.com/users/lain"
featured_url = "https://example.com/users/lain/collections/featured"
first_page_url = "#{featured_url}?page=true"
user_data =
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", "lain")
|> Jason.decode!()
|> Map.put("featured", featured_url)
|> Jason.encode!()
object_id = Ecto.UUID.generate()
object_url = "https://example.com/objects/#{object_id}"
featured_data =
Jason.encode!(%{
"id" => featured_url,
"type" => "OrderedCollection",
"first" => first_page_url
})
first_page_data =
Jason.encode!(%{
"id" => first_page_url,
"type" => "OrderedCollectionPage",
"partOf" => featured_url,
"orderedItems" => [object_url]
})
object_data =
"test/fixtures/statuses/note.json"
|> File.read!()
|> String.replace("{{object_id}}", object_id)
|> String.replace("{{nickname}}", "lain")
Tesla.Mock.mock(fn
%{
method: :get,
url: ^ap_id
} ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}
%{
method: :get,
url: ^featured_url
} ->
%Tesla.Env{
status: 200,
body: featured_data,
headers: [{"content-type", "application/activity+json"}]
}
%{
method: :get,
url: ^first_page_url
} ->
%Tesla.Env{
status: 200,
body: first_page_data,
headers: [{"content-type", "application/activity+json"}]
}
%{
method: :get,
url: ^object_url
} ->
%Tesla.Env{
status: 200,
body: object_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
refute capture_log(fn ->
{:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
assert_enqueued(
worker: Pleroma.Workers.RemoteFetcherWorker,
args: %{
"op" => "fetch_remote",
"id" => object_url,
"depth" => 1
}
)
Pleroma.Tests.ObanHelpers.perform_all()
assert user.featured_address == featured_url
assert Map.has_key?(user.pinned_objects, object_url)
assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
end) =~ "Could not parse featured collection"
end
test "fetches user birthday information from misskey" do
user_id = "https://misskey.io/@mkljczk"
@ -2818,4 +2917,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
"first" => "https://social.example/users/alice/collections/featured?page=true"
})
end
test "fetch_and_prepare_featured_from_ap_id handles embedded first collection pages" do
featured_url = "https://social.example/users/alice/collections/featured"
first_page_url = "#{featured_url}?page=true"
object_url = "https://social.example/objects/1"
featured_data =
Jason.encode!(%{
"id" => featured_url,
"type" => "OrderedCollection",
"first" => %{
"id" => first_page_url,
"type" => "OrderedCollectionPage",
"partOf" => featured_url,
"orderedItems" => [object_url]
}
})
Tesla.Mock.mock(fn
%{method: :get, url: ^featured_url} ->
%Tesla.Env{
status: 200,
body: featured_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
refute capture_log(fn ->
assert {:ok, pinned_objects} =
ActivityPub.fetch_and_prepare_featured_from_ap_id(featured_url)
assert Map.has_key?(pinned_objects, object_url)
end) =~ "Could not parse featured collection"
end
end

View file

@ -577,6 +577,298 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
end
describe "GET /api/v2/notifications" do
test "groups favourite notifications for the same status" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user1 = insert(:user)
other_user2 = insert(:user)
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
{:ok, _} = CommonAPI.favorite(status.id, other_user1)
{:ok, _} = CommonAPI.favorite(status.id, other_user2)
notifications = Repo.all(Notification)
notification_ids = Enum.map(notifications, &to_string(&1.id))
assert [persisted_group_key] = notifications |> Enum.map(& &1.group_key) |> Enum.uniq()
assert is_binary(persisted_group_key)
result =
conn
|> get("/api/v2/notifications")
|> json_response_and_validate_schema(200)
assert [%{"id" => account_id1}, %{"id" => account_id2}] = result["accounts"]
assert account_id1 in [other_user1.id, other_user2.id]
assert account_id2 in [other_user1.id, other_user2.id]
assert [%{"id" => status_id}] = result["statuses"]
assert status_id == status.id
assert [group] = result["notification_groups"]
assert group["type"] == "favourite"
assert group["notifications_count"] == 2
assert group["status_id"] == status.id
assert group["most_recent_notification_id"] in notification_ids
assert group["page_min_id"] in notification_ids
assert group["page_max_id"] in notification_ids
assert group["latest_page_notification_at"]
assert Enum.sort(group["sample_account_ids"]) == Enum.sort([other_user1.id, other_user2.id])
group_key = group["group_key"]
assert group_key == persisted_group_key
assert [%{"group_key" => ^group_key}, %{"group_key" => ^group_key}] =
conn
|> get("/api/v1/notifications")
|> json_response_and_validate_schema(200)
assert %{"notification_groups" => [shown_group]} =
conn
|> get("/api/v2/notifications/#{group_key}")
|> json_response_and_validate_schema(200)
assert shown_group["group_key"] == group_key
assert shown_group["notifications_count"] == 2
refute Map.has_key?(shown_group, "page_min_id")
refute Map.has_key?(shown_group, "page_max_id")
refute Map.has_key?(shown_group, "latest_page_notification_at")
end
test "round-trips ungrouped group keys when grouped_types excludes a type" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
{:ok, _} = CommonAPI.favorite(status.id, other_user)
%{"notification_groups" => [%{"group_key" => "ungrouped-" <> _ = group_key}]} =
conn
|> get("/api/v2/notifications?grouped_types[]=reblog")
|> json_response_and_validate_schema(200)
assert %{
"notification_groups" => [%{"group_key" => ^group_key, "notifications_count" => 1}]
} =
conn
|> get("/api/v2/notifications/#{group_key}")
|> json_response_and_validate_schema(200)
end
test "keeps legacy notifications without persisted group keys ungrouped" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user1 = insert(:user)
other_user2 = insert(:user)
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
{:ok, favorite1} = CommonAPI.favorite(status.id, other_user1)
{:ok, favorite2} = CommonAPI.favorite(status.id, other_user2)
notification_ids =
[favorite1, favorite2]
|> Enum.map(&get_notification_id_by_activity/1)
Repo.update_all(Notification, set: [group_key: nil])
expected_group_keys = Enum.map(notification_ids, &"ungrouped-#{&1}")
assert [%{"group_key" => group_key1}, %{"group_key" => group_key2}] =
conn
|> get("/api/v1/notifications")
|> json_response_and_validate_schema(200)
assert Enum.sort([group_key1, group_key2]) == Enum.sort(expected_group_keys)
%{"notification_groups" => groups} =
conn
|> get("/api/v2/notifications")
|> json_response_and_validate_schema(200)
assert Enum.sort(Enum.map(groups, & &1["group_key"])) == Enum.sort(expected_group_keys)
assert Enum.all?(groups, &(&1["notifications_count"] == 1))
group_key = List.first(expected_group_keys)
assert %{
"notification_groups" => [%{"group_key" => ^group_key, "notifications_count" => 1}]
} =
conn
|> get("/api/v2/notifications/#{group_key}")
|> json_response_and_validate_schema(200)
end
test "paginates notification groups instead of raw notifications" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user1 = insert(:user)
other_user2 = insert(:user)
other_user3 = insert(:user)
{:ok, older_status} = CommonAPI.post(user, %{status: "older"})
{:ok, _} = CommonAPI.favorite(older_status.id, other_user3)
{:ok, newer_status} = CommonAPI.post(user, %{status: "newer"})
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user1)
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user2)
older_status_id = older_status.id
newer_status_id = newer_status.id
%{"notification_groups" => groups} =
conn
|> get("/api/v2/notifications?limit=2")
|> json_response_and_validate_schema(200)
assert [
%{"status_id" => ^newer_status_id, "notifications_count" => 2},
%{"status_id" => ^older_status_id, "notifications_count" => 1}
] = groups
end
test "uses group-level cursors when paginating notification groups" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user1 = insert(:user)
other_user2 = insert(:user)
other_user3 = insert(:user)
{:ok, older_status} = CommonAPI.post(user, %{status: "older"})
{:ok, _} = CommonAPI.favorite(older_status.id, other_user3)
{:ok, newer_status} = CommonAPI.post(user, %{status: "newer"})
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user1)
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user2)
older_status_id = older_status.id
newer_status_id = newer_status.id
assert %{
"notification_groups" => [
%{
"status_id" => ^newer_status_id,
"notifications_count" => 2,
"most_recent_notification_id" => cursor
}
]
} =
conn
|> get("/api/v2/notifications?limit=1")
|> json_response_and_validate_schema(200)
assert %{
"notification_groups" => [
%{"status_id" => ^older_status_id, "notifications_count" => 1}
]
} =
conn
|> get("/api/v2/notifications?limit=1&max_id=#{cursor}")
|> json_response_and_validate_schema(200)
end
test "returns total notification count for a partially represented group" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user1 = insert(:user)
other_user2 = insert(:user)
other_user3 = insert(:user)
{:ok, grouped_status} = CommonAPI.post(user, %{status: "grouped"})
{:ok, _} = CommonAPI.favorite(grouped_status.id, other_user1)
{:ok, other_status} = CommonAPI.post(user, %{status: "other"})
{:ok, _} = CommonAPI.favorite(other_status.id, other_user3)
{:ok, _} = CommonAPI.favorite(grouped_status.id, other_user2)
grouped_status_id = grouped_status.id
assert %{
"notification_groups" => [
%{"status_id" => ^grouped_status_id, "notifications_count" => 2}
]
} =
conn
|> get("/api/v2/notifications?limit=1")
|> json_response_and_validate_schema(200)
end
test "counts unread notification groups" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user1 = insert(:user)
other_user2 = insert(:user)
mentioner = insert(:user)
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
{:ok, _} = CommonAPI.favorite(status.id, other_user1)
{:ok, _} = CommonAPI.favorite(status.id, other_user2)
{:ok, _} = CommonAPI.post(mentioner, %{status: "hi @#{user.nickname}"})
assert %{"count" => 2} =
conn
|> get("/api/v2/notifications/unread_count")
|> json_response_and_validate_schema(200)
end
test "counts unread notification groups newer than the notification marker" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
mentioner = insert(:user)
{:ok, status} = CommonAPI.post(user, %{status: "older"})
{:ok, favorite_activity} = CommonAPI.favorite(status.id, other_user)
marker_id = get_notification_id_by_activity(favorite_activity)
{:ok, _} = CommonAPI.post(mentioner, %{status: "newer @#{user.nickname}"})
{:ok, _} =
Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => marker_id}})
assert %{"count" => 1} =
conn
|> get("/api/v2/notifications/unread_count")
|> json_response_and_validate_schema(200)
end
test "does not preserve stale cursor params in grouped notification link headers" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, activity1} = CommonAPI.post(other_user, %{status: "one @#{user.nickname}"})
{:ok, _activity2} = CommonAPI.post(other_user, %{status: "two @#{user.nickname}"})
{:ok, activity3} = CommonAPI.post(other_user, %{status: "three @#{user.nickname}"})
notification1_id = get_notification_id_by_activity(activity1)
notification3_id = get_notification_id_by_activity(activity3)
conn = get(conn, "/api/v2/notifications?since_id=#{notification1_id}&limit=1")
assert [link_header] = get_resp_header(conn, "link")
assert link_header =~ ~r/max_id=#{notification3_id}/
refute link_header =~ "since_id="
end
test "lists accounts from a notification group" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
{other_users, %{"group_key" => group_key, "sample_account_ids" => sample_account_ids}} =
create_favourite_notification_group(user, conn)
assert length(sample_account_ids) == 8
account_ids =
conn
|> get("/api/v2/notifications/#{group_key}/accounts")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert Enum.sort(account_ids) == Enum.sort(Enum.map(other_users, & &1.id))
end
test "dismisses a notification group" do
%{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"])
{_other_users, %{"group_key" => group_key}} =
create_favourite_notification_group(user, conn)
assert %{} =
conn
|> post("/api/v2/notifications/#{group_key}/dismiss")
|> json_response_and_validate_schema(200)
assert [] = Notification.for_user(user)
end
end
test "doesn't see notifications after muting user with notifications" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
user2 = insert(:user)
@ -708,6 +1000,29 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
|> to_string()
end
defp create_favourite_notification_group(user, conn) do
other_users = insert_list(9, :user)
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
Enum.each(other_users, fn other_user ->
{:ok, _} = CommonAPI.favorite(status.id, other_user)
end)
%{
"notification_groups" => [
%{
"notifications_count" => 9
} = group
]
} =
conn
|> get("/api/v2/notifications")
|> json_response_and_validate_schema(200)
{other_users, group}
end
defp params_to_query(%{} = params) do
Enum.map_join(params, "&", fn
{k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")

View file

@ -0,0 +1,675 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do
use Pleroma.DataCase, async: true
import Pleroma.Factory
alias Pleroma.Language.LanguageDetectorMock
alias Pleroma.StaticStubbedConfigMock
alias Pleroma.Web.MastodonAPI.InstanceView
import Mox
@common_information_keys [
:languages,
:rules,
:title,
:version
]
# Used for filtering out values from configuration.json render in configuration2.json render
@configuration2_filter [
{:accounts,
[
:max_pinned_statuses,
:max_profile_fields,
:profile_field_name_limit,
:profile_field_value_limit
]},
{:statuses, [:characters_reserved_per_url]},
{:urls, [:streaming, :status]},
{:vapid, [:public_key]},
{:translation, [:enabled]},
{:timelines_access, [:live_feeds, :hashtag_feeds, :trending_link_feeds]}
]
@pleroma_configuration_filter [{:metadata, [:features, :federation]}]
@pleroma_configuration2_filter [
{:metadata,
[
:avatar_upload_limit,
:background_upload_limit,
:banner_upload_limit,
:background_image,
:chat_limit,
:description_limit,
:shout_limit
]}
]
@show_keys @common_information_keys ++
[
:uri,
:description,
:short_description,
:email,
:urls,
:stats,
:thumbnail,
:registrations,
:approval_required,
:contact_account,
:configuration,
# Extra (not present in Mastodon)
:max_toot_chars,
:max_media_attachments,
:poll_limits,
:upload_limit,
:avatar_upload_limit,
:background_upload_limit,
:banner_upload_limit,
:background_image,
:shout_limit,
:description_limit,
:chat_limit,
:pleroma
]
# Checked in other check_* functions
@show_filter @common_information_keys ++ [:contact_account, :configuration, :pleroma]
@show2_keys @common_information_keys ++
[
:domain,
:source_url,
:description,
:usage,
:thumbnail,
:configuration,
:registrations,
:contact,
# Extra (not present in Mastodon)
:pleroma
]
@show2_filter @common_information_keys ++
[:thumbnail, :configuration, :registrations, :contact, :pleroma]
@features [
"pleroma_api",
"mastodon_api",
"mastodon_api_grouped_notifications",
"mastodon_api_streaming",
"polls",
"v2_suggestions",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"editing",
"quote_posting",
"pleroma_emoji_reactions",
"pleroma_custom_emoji_reactions",
"pleroma_chat_messages",
"pleroma:pin_chats",
"pleroma:get:main/ostatus",
"pleroma:group_actors",
"pleroma:bookmark_folders",
"pleroma:block_expiration"
]
@configurable_features [
"blockers_visible",
"media_proxy",
"gopher",
"chat",
"shout",
"relay",
"safe_dm_mentions",
"exposable_reactions",
"profile_directory",
"pleroma:language_detection"
]
@configurable_features_flags [
[:activitypub, :blockers_visible],
[:media_proxy, :enabled],
[:gopher, :enabled],
[:shout, :enabled],
[:instance, :allow_relay],
[:instance, :safe_dm_mentions],
[:instance, :show_reactions],
[:instance, :profile_directory]
]
# Filters out values from one layer deep nested Maps.
# Filtering types:
# - :inclusive -> filters out everything that is not in the filters
# - :discriminative -> filters out what is in the filters
defp filter_render(input, filter, type, acc)
defp filter_render(_input_rest, [], type, acc) when type == :inclusive, do: acc
defp filter_render(input_rest, [], type, acc) when type == :discriminative,
do: Map.merge(input_rest, acc)
defp filter_render(input, filter, type, acc) do
{{filter_key, filters}, filter_rest} = List.pop_at(filter, 0)
{config_key_values, input_rest} = Map.pop!(input, filter_key)
filtered =
case type do
:inclusive ->
Map.reject(config_key_values, fn {key, _} -> key not in filters end)
:discriminative ->
Map.reject(config_key_values, fn {key, _} -> key in filters end)
end
filter_render(input_rest, filter_rest, type, Map.put(acc, filter_key, filtered))
end
defp check_common_information(info) do
filtered_info = Map.reject(info, fn {key, _value} -> key not in @common_information_keys end)
expected = %{
languages: ["en"],
rules: InstanceView.render("rules.json", %{}),
title: "Pleroma",
version:
InstanceView.mastodon_api_level() <>
" (compatible; #{Pleroma.Application.named_version()})"
}
Map.equal?(expected, filtered_info)
end
defp check_thumbnail2(info) do
thumbnail = info[:thumbnail]
expected = %{
url:
URI.merge(Pleroma.Web.Endpoint.url(), "http://localhost:4001/instance/thumbnail.jpeg")
|> to_string
}
expected == thumbnail
end
defp check_contact(info, user) do
contact = info[:contact]
expected = %{
email: "admin@example.com",
account: Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
}
Map.equal?(expected, contact)
end
defp check_contact_account(info, user) do
contact = info[:contact_account]
view = Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
contact == view
end
defp check_configuration(info) do
configuration = info[:configuration]
expected = %{
accounts: %{
max_featured_tags: 0
},
statuses: %{
max_characters: 5_000,
max_media_attachments: 1_000
},
media_attachments: %{
image_size_limit: 16_000_000,
video_size_limit: 16_000_000,
supported_mime_types: ["application/octet-stream"]
},
polls: %{
max_options: 20,
max_characters_per_option: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
}
}
Map.equal?(expected, configuration)
end
defp check_configuration2(info) do
configuration =
info[:configuration]
|> filter_render(@configuration2_filter, :inclusive, %{})
expected = %{
accounts: %{
max_pinned_statuses: 1,
max_profile_fields: 10,
profile_field_name_limit: 512,
profile_field_value_limit: 2048
},
statuses: %{
characters_reserved_per_url: 0
},
urls: %{
streaming: Pleroma.Web.Endpoint.websocket_url(),
status: nil
},
vapid: %{
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
},
translation: %{enabled: true},
timelines_access: %{
live_feeds: %{local: "public", remote: "public"},
hashtag_feeds: %{local: "public", remote: "public"},
# not implemented in Pleroma
trending_link_feeds: %{
local: "disabled",
remote: "disabled"
}
}
}
Map.equal?(expected, configuration)
end
defp check_registrations2(info) do
registrations = info[:registrations]
expected = %{
enabled: true,
approval_required: false,
message: nil,
url: nil
}
Map.equal?(expected, registrations)
end
defp check_pleroma_configuration(info) do
configuration =
info[:pleroma]
|> filter_render(@pleroma_configuration_filter, :discriminative, %{})
expected = %{
metadata: %{
account_activation_required: false,
fields_limits: %{
max_fields: 10,
max_remote_fields: 20,
name_length: 512,
value_length: 2048
},
post_formats: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode",
"text/x.misskeymarkdown"
],
birthday_required: false,
birthday_min_age: 0,
translation: %{
source_languages: ["en", "pl"],
target_languages: ["en", "pl"]
},
base_urls: %{},
markup: %{
allow_inline_images: true,
allow_headings: false,
allow_tables: false
}
},
stats: %{mau: Pleroma.User.active_user_count()},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
}
Map.equal?(expected, configuration)
end
defp check_pleroma_configuration2(info) do
configuration =
info[:pleroma]
|> filter_render(@pleroma_configuration2_filter, :inclusive, %{})
expected = %{
metadata: %{
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
background_image: Pleroma.Web.Endpoint.url() <> "/images/city.jpg",
chat_limit: 5_000,
description_limit: 5_000,
shout_limit: 5_000
}
}
Map.equal?(expected, configuration)
end
defp check_show(info) do
filter = @show_filter
filtered_info = Map.reject(info, fn {key, _value} -> key in filter end)
expected = %{
uri: Pleroma.Web.WebFinger.host(),
description: "Pleroma: An efficient and flexible fediverse server",
short_description: "",
email: "admin@example.com",
urls: %{
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
thumbnail: "http://localhost:4001/instance/thumbnail.jpeg",
registrations: true,
approval_required: false,
max_toot_chars: 5_000,
max_media_attachments: 1_000,
poll_limits: %{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
},
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
background_image: Pleroma.Web.Endpoint.url() <> "/images/city.jpg",
shout_limit: 5_000,
description_limit: 5_000,
chat_limit: 5_000
}
Map.equal?(expected, filtered_info)
end
defp check_show2(info) do
filter = @show2_filter
filtered_info = Map.reject(info, fn {key, _value} -> key in filter end)
expected = %{
domain: Pleroma.Web.WebFinger.host(),
source_url: Pleroma.Application.repository(),
description: "",
usage: %{users: %{active_month: Pleroma.User.active_user_count()}}
}
Map.equal?(expected, filtered_info)
end
# When this fails, a new feature flag was probably added. Add it to the macros above.
describe "uncofigurable features" do
setup do: Enum.each(@configurable_features_flags, &clear_config(&1, false))
test "returns always enabled features" do
features = @features
assert Enum.sort(InstanceView.features()) == Enum.sort(features)
end
end
describe "all features" do
setup do
Enum.each(@configurable_features_flags, &clear_config(&1, true))
# Stub the StaticStubbedConfigMock to return our mock for the provider
StaticStubbedConfigMock
|> stub(:get, fn
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
_other -> nil
end)
# Stub the LanguageDetectorMock with default implementations
LanguageDetectorMock
|> stub(:missing_dependencies, fn -> [] end)
|> stub(:configured?, fn -> true end)
:ok
end
test "returns all features including configurable ones" do
features = @features ++ @configurable_features
assert Enum.sort(InstanceView.features()) == Enum.sort(features)
end
end
describe "federation/0" do
setup do: clear_config([:mrf, :transparency], true)
test "quarantined instances" do
clear_config([:instance, :quarantined_instances], [{"quarantine.example.com", "crimes"}])
output = InstanceView.federation()
assert %{quarantined_instances: ["quarantine.example.com"]} = output
assert %{
quarantined_instances_info: %{
"quarantined_instances" => %{"quarantine.example.com" => %{"reason" => "crimes"}}
}
} = output
end
test "rejected instances" do
clear_config([:instance, :rejected_instances], [
{"rejected.example.com", "not enough #cofe posting"}
])
output = InstanceView.federation()
assert %{
rejected_instances: %{
"rejected.example.com" => %{"reason" => "not enough #cofe posting"}
}
} = output
end
test "federating" do
clear_config([:instance, :federating], true)
output = InstanceView.federation()
assert %{enabled: true} = output
end
test "transparency" do
clear_config([:instance, :quarantined_instances], [{"quarantine.example.com", "crimes"}])
clear_config([:instance, :federating], true)
clear_config([:mrf, :transparency], false)
clear_config([:instance, :rejected_instances], [
{"rejected.example.com", "not enough #cofe posting"}
])
output = InstanceView.federation()
assert %{enabled: true} == output
end
test "MRFs" do
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
clear_config([:mrf_simple, :reject], [{"rejected.example.com", "not enough #cofe posting"}])
clear_config([:mrf, :transparency_exclusions], [
{"media1.example.com", "the Fediverse doesn't need to know"}
])
clear_config([:mrf_simple, :media_removal], [
{"media1.example.com", "NSFW"},
{"media2.example.com", "usual suspects"}
])
output = InstanceView.federation()
expected = %{
mrf_simple: %{
reject: ["rejected.example.com"],
media_removal: ["media2.example.com"]
},
mrf_hashtag: %{
sensitive: ["nsfw"]
},
exclusions: true,
mrf_policies: ["SimplePolicy", "HashtagPolicy"],
mrf_simple_info: %{
reject: %{
"rejected.example.com" => %{"reason" => "not enough #cofe posting"}
},
media_removal: %{"media2.example.com" => %{"reason" => "usual suspects"}}
}
}
assert expected.mrf_simple.reject == output.mrf_simple.reject
assert expected.mrf_simple.media_removal == output.mrf_simple.media_removal
assert expected.mrf_simple_info == output.mrf_simple_info
assert expected.exclusions == output.exclusions
end
end
describe "render show.json/show2.json" do
setup do
Pleroma.Rule.create(%{text: "rule 1"})
Pleroma.Rule.create(%{text: "rule 2"})
user = insert(:user)
# contact in show2.json
clear_config([:instance, :contact_username], user.nickname)
# Pleroma.Language.Translation.configured?() -> true
clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
# Deprecated, but still used in view
clear_config([:instance, :chat_limit], 5_000)
%{user: user}
end
test "renders show.json properly", %{user: user} do
output = InstanceView.render("show.json", %{})
expected_keys = Enum.sort(@show_keys)
assert check_common_information(output)
assert check_show(output)
assert check_contact_account(output, user)
assert check_configuration(output)
assert check_pleroma_configuration(output)
assert expected_keys == Enum.sort(Map.keys(output))
end
test "renders show2.json properly", %{user: user} do
output = InstanceView.render("show2.json", %{})
expected_keys = Enum.sort(@show2_keys)
assert check_common_information(output)
assert check_show2(output)
assert check_thumbnail2(output)
assert check_configuration2(output)
assert check_registrations2(output)
assert check_contact(output, user)
assert check_pleroma_configuration2(output)
assert expected_keys == Enum.sort(Map.keys(output))
end
end
describe "render rule.json" do
setup do
rule = Pleroma.Rule.create(%{text: "hambaga"})
%{rule: rule}
end
test "renders properly", %{rule: rule} do
output = InstanceView.render("rule.json", %{rule: rule})
assert %{text: "hambaga"} = output
end
end
describe "render rules.json" do
setup do
Pleroma.Rule.create(%{text: "rule 1"})
Pleroma.Rule.create(%{text: "rule 2"})
Pleroma.Rule.create(%{text: "rule 3"})
Pleroma.Rule.create(%{text: "rule 4"})
:ok
end
test "renders properly" do
output = InstanceView.render("rules.json", %{})
keys = Enum.map(output, fn %{text: text} -> text end)
assert length(keys) == 4
assert Enum.all?(output, fn %{text: "rule " <> num} -> String.to_integer(num) end)
end
end
describe "render domain_blocks.json" do
setup do
clear_config([:mrf, :transparency], true)
clear_config([:mrf_simple, :media_removal], [{"media1.example.com", "NSFW"}])
clear_config([:mrf, :transparency_exclusions], [
{"removed1.example.com", "the Fediverse doesn't need to know"}
])
clear_config([:mrf_simple, :reject], [
{"rejected.example.com", "not enough #cofe posting"},
{"rejected-without-reason.example.com", ""}
])
clear_config([:mrf_simple, :federated_timeline_removal], [
{"removed1.example.com", "spam"},
{"removed2.example.com", "more spam"}
])
:ok
end
test "renders properly" do
output = InstanceView.render("domain_blocks.json", %{})
# Media removal is not in @block_severities in the view.
# NOTE: Order of these two matters!
expected = [
%{
domain: "removed2.example.com",
comment: "more spam",
severity: "silence",
digest: "5f6649cfc780fac5172dbbcc8a552953f6621a14368d82728b6e8faf51295dc0"
},
%{
domain: "rejected.example.com",
comment: "not enough #cofe posting",
severity: "suspend",
digest: "c15082d347fb7eccd251e199a98dcdce94a8dfc1d33d6ece80889eed374b6f92"
},
%{
domain: "rejected-without-reason.example.com",
severity: "suspend",
digest: "48297155018c626c8867ca0bba4a2621eccdf51c6f7ac91b7099ed8b2ee9863f"
}
]
assert expected == output
end
end
describe "render translation_languages.json" do
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
test "returns language matrix" do
output = InstanceView.render("translation_languages.json", %{})
assert %{"en" => ["pl"], "pl" => ["en"]} == output
end
end
end

View file

@ -98,10 +98,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
{:ok, favorite_activity} = CommonAPI.favorite(create_activity.id, another_user)
{:ok, [notification]} = Notification.create_notifications(favorite_activity)
create_activity = Activity.get_by_id(create_activity.id)
assert is_binary(notification.group_key)
expected = %{
id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
group_key: Notification.group_key(notification),
pleroma: %{is_seen: false, is_muted: false},
type: "favourite",
account: AccountView.render("show.json", %{user: another_user, for: user}),
@ -119,10 +120,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
{:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user)
{:ok, [notification]} = Notification.create_notifications(reblog_activity)
reblog_activity = Activity.get_by_id(create_activity.id)
assert is_binary(notification.group_key)
expected = %{
id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
group_key: Notification.group_key(notification),
pleroma: %{is_seen: false, is_muted: false},
type: "reblog",
account: AccountView.render("show.json", %{user: another_user, for: user}),
@ -138,10 +140,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
followed = insert(:user)
{:ok, followed, follower, _activity} = CommonAPI.follow(followed, follower)
notification = Notification |> Repo.one() |> Repo.preload(:activity)
assert is_binary(notification.group_key)
expected = %{
id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
group_key: Notification.group_key(notification),
pleroma: %{is_seen: false, is_muted: false},
type: "follow",
account: AccountView.render("show.json", %{user: follower, for: followed}),
@ -333,7 +336,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{
id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
group_key: Notification.group_key(notification),
pleroma: %{is_seen: true, is_muted: true},
type: "favourite",
account: AccountView.render("show.json", %{user: another_user, for: user}),

View file

@ -160,6 +160,7 @@ defmodule Pleroma.Web.NodeInfoTest do
default_features = [
"pleroma_api",
"mastodon_api",
"mastodon_api_grouped_notifications",
"mastodon_api_streaming",
"polls",
"pleroma_explicit_addressing",

View file

@ -5,12 +5,22 @@
defmodule Pleroma.Web.RichMedia.BackfillTest do
use Pleroma.DataCase
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.RichMedia.Backfill
alias Pleroma.Web.RichMedia.Card
import Mox
import Pleroma.Factory
setup_all do: clear_config([:rich_media, :enabled], true)
setup do
clear_config([:rich_media, :enabled], true)
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache)
:ok
end
test "sets a negative cache entry for an error" do
url = "https://bad.example.com/"
@ -23,4 +33,139 @@ defmodule Pleroma.Web.RichMedia.BackfillTest do
Backfill.run(%{"url" => url})
end
test "sets a warm_cache entry" do
url = "https://good.example.com/"
url_hash = Card.url_to_hash(url)
Tesla.Mock.mock(fn %{url: ^url} ->
{:ok,
%Tesla.Env{
status: 200,
body: "<head><meta name=\"twitter:title\" content=\"Cofe\"></head>"
}}
end)
Pleroma.CachexMock
|> expect(:put, fn :rich_media_cache,
^url_hash,
%Pleroma.Web.RichMedia.Card{url_hash: ^url_hash} ->
{:ok, true}
end)
Backfill.run(%{"url" => url})
end
test "streams out update when stream == true" do
url = "https://example.com"
user = insert(:user)
Tesla.Mock.mock(fn %{url: ^url} ->
{:ok,
%Tesla.Env{
status: 200,
body: "<head><meta name=\"twitter:title\" content=\"Cofe\"></head>"
}}
end)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"})
Pleroma.CachexMock
|> expect(:put, fn :rich_media_cache, _, _ -> {:ok, true} end)
Pleroma.Web.ActivityPub.ActivityPubMock
|> expect(:stream_out, fn %Pleroma.Activity{id: id} ->
assert id == activity.id
:ok
end)
Backfill.run(%{"url" => url, "activity_id" => activity.id, "stream" => true})
end
test "does not stream out update when stream == false" do
url = "https://example.com"
user = insert(:user)
Tesla.Mock.mock(fn %{url: ^url} ->
{:ok,
%Tesla.Env{
status: 200,
body: "<head><meta name=\"twitter:title\" content=\"Cofe\"></head>"
}}
end)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"})
Pleroma.CachexMock
|> expect(:put, fn :rich_media_cache, _, _ -> {:ok, true} end)
Pleroma.Web.ActivityPub.ActivityPubMock
|> deny(:stream_out, 1)
Backfill.run(%{"url" => url, "activity_id" => "#{activity.data["id"]}", "stream" => false})
end
# NOTE: Below two MastoAPI tests cover almost the same code paths.
# index.json will always prefetch rich media, while show.json will try to get the card and
# fetch it when it isn't cached (both use Card.get_by_activity in the end).
# So if index.json doesn't fetch the rich media, show.json will when it renders the post,
# hence why index.json test will only call ActivityPub.stream_out twice,
# if streaming is re-enabled for in both.
test "does not stream out in MastoAPI StatusView index" do
url = "https://example.com"
user = insert(:user)
Tesla.Mock.mock(fn %{url: ^url} ->
{:ok,
%Tesla.Env{
status: 200,
body: "<head><meta name=\"twitter:title\" content=\"Cofe\"></head>"
}}
end)
# CommonAPI federation processing will stream out once as a new post
Pleroma.Web.ActivityPub.ActivityPubMock
|> expect(:stream_out, 1, fn _ -> :ok end)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"})
ObanHelpers.perform_all()
# Clear cache to force backfill below
Pleroma.Activity.HTML.invalidate_cache_for(activity.id)
Pleroma.Web.RichMedia.Card.delete(url)
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: [activity],
as: :activity
})
ObanHelpers.perform_all()
end
test "does not stream out in MastoAPI StatusView show" do
url = "https://example.com"
user = insert(:user)
Tesla.Mock.mock(fn %{url: ^url} ->
{:ok,
%Tesla.Env{
status: 200,
body: "<head><meta name=\"twitter:title\" content=\"Cofe\"></head>"
}}
end)
# CommonAPI federation processing will stream out once as a new post
Pleroma.Web.ActivityPub.ActivityPubMock
|> expect(:stream_out, 1, fn _ -> :ok end)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe #{url}"})
ObanHelpers.perform_all()
# Clear cache to force backfill below
Pleroma.Activity.HTML.invalidate_cache_for(activity.id)
Pleroma.Web.RichMedia.Card.delete(url)
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity)
ObanHelpers.perform_all()
end
end