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

This commit is contained in:
Lain Soykaf 2026-07-27 11:34:31 +04:00
commit b4c624911c
No known key found for this signature in database
1163 changed files with 40459 additions and 6082 deletions

View file

@ -261,23 +261,27 @@ defmodule Pleroma.ActivityTest do
test "add_by_params_query/3" do
user = insert(:user)
note = insert(:note_activity, user: user)
note_activity = insert(:note_activity, user: user)
insert(:add_activity, user: user, note: note)
insert(:add_activity, user: user, note: note)
insert(:add_activity, user: user, note_activity: note_activity)
insert(:add_activity, user: user, note_activity: note_activity)
insert(:add_activity, user: user)
assert Repo.aggregate(Activity, :count, :id) == 4
assert Repo.aggregate(Activity, :count, :id) == 5
add_query =
Activity.add_by_params_query(note.data["object"], user.ap_id, user.featured_address)
Activity.add_by_params_query(
note_activity.data["object"],
user.ap_id,
user.featured_address
)
assert Repo.aggregate(add_query, :count, :id) == 2
Repo.delete_all(add_query)
assert Repo.aggregate(add_query, :count, :id) == 0
assert Repo.aggregate(Activity, :count, :id) == 2
assert Repo.aggregate(Activity, :count, :id) == 3
end
describe "associated_object_id() sql function" do

View file

@ -0,0 +1,39 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ApplicationTest do
use Pleroma.DataCase
import ExUnit.CaptureLog
# TODO HACK: Temporarily disabled.
# Currently causes a bunch of warnings about :warn being deprecated in logs.
# ex_syslogger needs to either be updated upstream or forked.
describe "config :pleroma, :logger, backends: [{:ex_syslogger, :ex_syslogger}]" do
setup do
clear_config([:logger], backends: [{:ex_syslogger, :ex_syslogger}])
end
@tag :skip
test "is handled" do
log = capture_log(fn -> Pleroma.Application.configure_logger() end)
assert log =~
"Configuration {:ex_syslogger, :ex_syslogger} is incorrect. Use {ExSyslogger, :ex_syslogger} instead!"
assert log =~ "Successfully added logger backend: {ExSyslogger, :ex_syslogger}"
end
end
describe "config :pleroma, :logger, :backends: [:console]" do
setup do
clear_config([:logger], backends: [:console])
end
test "emits a warning" do
assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~
":console is no longer considered a backend and is enabled by default"
end
end
end

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.DeprecationWarningsTest do
use ExUnit.Case
use ExUnit.Case, async: false
use Pleroma.Tests.Helpers
import ExUnit.CaptureLog
@ -388,4 +388,62 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
end) =~
"Your config is using the old namespace for the Shoutbox configuration."
end
describe "check_deprecated_logger_config" do
setup do
initial_console = Application.get_env(:logger, :console, nil)
initial_backends = Application.get_env(:logger, :backends, nil)
# NOTE: The Logger does not get reconfigured when running in the test env,
# the added and on_exit removed settings below should have no effect besides
# triggering the deprecation warnings
Application.put_env(:logger, :console, level: :all)
Application.put_env(:logger, :backends, [:console])
on_exit(fn ->
if initial_console do
Application.put_env(:logger, :console, initial_console)
else
Application.delete_env(:logger, :console)
end
if initial_backends do
Application.put_env(:logger, :backends, initial_backends)
else
Application.delete_env(:logger, :backends)
end
end)
end
test "warns on deprecated syntax" do
log =
capture_log(fn ->
Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config()
end)
assert log =~
"""
!!!DEPRECATION WARNING!!!
Your configuration is using deprecated syntax for configuring backends of Elixir's logger.
`config :logger, backends: [...]` is deprecated syntax due to changes in Elixir.
Please update your configuration at your earliest convenience to use:
`config :pleroma, :logger, backends: [...]`
Note: `:console` is no longer considered a backend and is used by default, you can disable it using:
`config :logger, :default_handler: false`
"""
assert log =~
"""
!!!DEPRECATION WARNING!!!
Your configuration is using deprecated syntax for configuring logging to console.
`config :logger, :console` is deprecated syntax due to changes in Elixir.
Please update your configuration at your earliest convenience to use
`config :logger, default_handler` and `config :logger, :default_formatter`.
Note: `:default_handler` is used only for the `level` setting. All other configurations go under
`:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility
"""
end
end
end

View file

@ -71,6 +71,31 @@ defmodule Pleroma.Config.TransferTaskTest do
assert assets_env[:mascots] == [a: 1, b: 2]
end
test "does not transfer static search configuration" do
search_config = Application.get_env(:pleroma, Pleroma.Search)
parade_config = Application.get_env(:pleroma, Pleroma.Search.ParadeDB)
clear_config(:database_config_blacklist, [])
insert(:config, key: Pleroma.Search, value: [module: Pleroma.Search.ParadeDB])
insert(:config,
key: Pleroma.Search.ParadeDB,
value: [url: "postgres://localhost/paradedb"]
)
insert(:config,
key: Pleroma.Search.ParadeDB.Repo,
value: [pool_size: 50]
)
TransferTask.start_link([])
assert Application.get_env(:pleroma, Pleroma.Search) == search_config
assert Application.get_env(:pleroma, Pleroma.Search.ParadeDB) == parade_config
assert Application.get_env(:pleroma, Pleroma.Search.ParadeDB.Repo)[:pool_size] == 2
end
describe "pleroma restart" do
setup do
on_exit(fn ->

View file

@ -36,6 +36,20 @@ defmodule Pleroma.ConfigDBTest do
assert config[:goose][:webhook_url] == "https://gander.com/"
end
test "get_all_as_keyword/1 excludes static search configuration" do
insert(:config, key: Pleroma.Search, value: [module: Pleroma.Search.ParadeDB])
insert(:config, key: Pleroma.Search.ParadeDB, value: [url: "postgres://example/db"])
insert(:config, key: Pleroma.Search.ParadeDB.Repo, value: [pool_size: 50])
insert(:config, key: :instance, value: [name: "Pleroma"])
config = ConfigDB.get_all_as_keyword(exclude_static: true)
refute config[:pleroma][Pleroma.Search]
refute config[:pleroma][Pleroma.Search.ParadeDB]
refute config[:pleroma][Pleroma.Search.ParadeDB.Repo]
assert config[:pleroma][:instance] == [name: "Pleroma"]
end
describe "update_or_create/1" do
test "common" do
config = insert(:config)
@ -174,6 +188,22 @@ defmodule Pleroma.ConfigDBTest do
assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
end
test "rejects invalid :rate_limit values (e.g. empty-string scale from AdminFE)" do
assert {:error, _changeset} =
ConfigDB.update_or_create(%{
group: ":pleroma",
key: ":rate_limit",
value: [
%{
"tuple" => [
":statuses_actions",
[%{"tuple" => ["", 0]}, %{"tuple" => ["", ""]}]
]
}
]
})
end
end
describe "delete/1" do
@ -273,24 +303,28 @@ defmodule Pleroma.ConfigDBTest do
end
test "sigil" do
assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/
assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source ==
~r/comp[lL][aA][iI][nN]er/.source
end
test "link sigil" do
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source ==
~r/https:\/\/example.com/.source
end
test "link sigil with um modifiers" do
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") ==
~r/https:\/\/example.com/um
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um").source ==
~r/https:\/\/example.com/um.source
end
test "link sigil with i modifier" do
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source ==
~r/https:\/\/example.com/i.source
end
test "link sigil with s modifier" do
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source ==
~r/https:\/\/example.com/s.source
end
test "raise if valid delimiter not found" do
@ -460,11 +494,11 @@ defmodule Pleroma.ConfigDBTest do
test "complex keyword with sigil" do
assert ConfigDB.to_elixir_types([
%{"tuple" => [":federated_timeline_removal", []]},
%{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
%{"tuple" => [":reject", [~r/comp[lL][aA][iI][nN]er/.source]]},
%{"tuple" => [":replace", []]}
]) == [
federated_timeline_removal: [],
reject: [~r/comp[lL][aA][iI][nN]er/],
reject: [~r/comp[lL][aA][iI][nN]er/.source],
replace: []
]
end

View file

@ -332,7 +332,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
# When it's a reply from the blocked user
{:ok, _direct2} =
CommonAPI.post(blocked, %{
status: "reply",
status: "@#{third_user.nickname}, #{blocker.nickname} reply",
visibility: "direct",
in_reply_to_conversation_id: blocked_participation.id
})

View file

@ -14,7 +14,7 @@ defmodule Pleroma.ConversationTest do
setup_all do: clear_config([:instance, :federating], true)
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
@ -66,8 +66,10 @@ defmodule Pleroma.ConversationTest do
jafnhar = insert(:user, local: false)
tridi = insert(:user)
to = [har.nickname, jafnhar.nickname, tridi.nickname]
{:ok, activity} =
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"})
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct", to: to})
object = Pleroma.Object.normalize(activity, fetch: false)
context = object.data["context"]
@ -88,7 +90,8 @@ defmodule Pleroma.ConversationTest do
CommonAPI.post(jafnhar, %{
status: "Hey @#{har.nickname}",
visibility: "direct",
in_reply_to_status_id: activity.id
in_reply_to_status_id: activity.id,
to: to
})
object = Pleroma.Object.normalize(activity, fetch: false)
@ -112,7 +115,8 @@ defmodule Pleroma.ConversationTest do
CommonAPI.post(tridi, %{
status: "Hey @#{har.nickname}",
visibility: "direct",
in_reply_to_status_id: activity.id
in_reply_to_status_id: activity.id,
to: to
})
object = Pleroma.Object.normalize(activity, fetch: false)

View file

@ -0,0 +1,16 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.DependencyVersionTest do
use ExUnit.Case, async: true
test "uses majic 1.2" do
majic_version =
:majic
|> Application.spec(:vsn)
|> to_string()
assert Version.match?(majic_version, "~> 1.2")
end
end

View file

@ -0,0 +1,56 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMapTest do
use Pleroma.DataCase, async: true
alias Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap
test "it validates" do
data = %{
"en-US" => "mew mew",
"en-GB" => "meow meow"
}
assert {:ok, ^data} = ContentLanguageMap.cast(data)
end
test "it validates empty strings" do
data = %{
"en-US" => "mew mew",
"en-GB" => ""
}
assert {:ok, ^data} = ContentLanguageMap.cast(data)
end
test "it ignores non-strings within the map" do
data = %{
"en-US" => "mew mew",
"en-GB" => 123
}
assert {:ok, validated_data} = ContentLanguageMap.cast(data)
assert validated_data == %{"en-US" => "mew mew"}
end
test "it ignores bad locale codes" do
data = %{
"en-US" => "mew mew",
"en_GB" => "meow meow",
"en<<#@!$#!@%!GB" => "meow meow"
}
assert {:ok, validated_data} = ContentLanguageMap.cast(data)
assert validated_data == %{"en-US" => "mew mew"}
end
test "it complains with non-map data" do
assert :error = ContentLanguageMap.cast("mew")
assert :error = ContentLanguageMap.cast(["mew"])
assert :error = ContentLanguageMap.cast([%{"en-US" => "mew"}])
end
end

View file

@ -0,0 +1,29 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCodeTest do
use Pleroma.DataCase, async: true
alias Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode
test "it accepts language code" do
text = "pl"
assert {:ok, ^text} = LanguageCode.cast(text)
end
test "it accepts language code with region" do
text = "pl-PL"
assert {:ok, ^text} = LanguageCode.cast(text)
end
test "errors for invalid language code" do
assert {:error, :invalid_language} = LanguageCode.cast("ru_RU")
assert {:error, :invalid_language} = LanguageCode.cast(" ")
assert {:error, :invalid_language} = LanguageCode.cast("en-US\n")
end
test "errors for non-text" do
assert :error == LanguageCode.cast(42)
end
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Emoji.PackTest do
use Pleroma.DataCase
alias Pleroma.Emoji
alias Pleroma.Emoji.Pack
@emoji_path Path.join(
@ -12,6 +13,9 @@ defmodule Pleroma.Emoji.PackTest do
)
setup do
# Reload emoji to ensure a clean state
Emoji.reload()
pack_path = Path.join(@emoji_path, "dump_pack")
File.mkdir(pack_path)
@ -53,6 +57,63 @@ defmodule Pleroma.Emoji.PackTest do
assert updated_pack.files_count == 5
end
test "skips existing emojis when adding from zip file", %{pack: pack} do
# First, let's create a test pack with a "bear" emoji
test_pack_path = Path.join(@emoji_path, "test_bear_pack")
Pleroma.Backports.mkdir_p(test_pack_path)
# Create a pack.json file
File.write!(Path.join(test_pack_path, "pack.json"), """
{
"files": { "bear": "bear.png" },
"pack": {
"description": "Bear Pack", "homepage": "https://pleroma.social",
"license": "Test license", "share-files": true
}}
""")
# Copy a test image to use as the bear emoji
File.cp!(
Path.absname("test/instance_static/emoji/test_pack/blank.png"),
Path.join(test_pack_path, "bear.png")
)
# Load the pack to register the "bear" emoji in the global registry
{:ok, _bear_pack} = Pleroma.Emoji.Pack.load_pack("test_bear_pack")
# Reload emoji to make sure the bear emoji is in the global registry
Emoji.reload()
# Verify that the bear emoji exists in the global registry
assert Emoji.exist?("bear")
# Now try to add a zip file that contains an emoji with the same shortcode
file = %Plug.Upload{
content_type: "application/zip",
filename: "emojis.zip",
path: Path.absname("test/fixtures/emojis.zip")
}
{:ok, updated_pack} = Pack.add_file(pack, nil, nil, file)
# Verify that the "bear" emoji was skipped
refute Map.has_key?(updated_pack.files, "bear")
# Other emojis should be added
assert Map.has_key?(updated_pack.files, "a_trusted_friend-128")
assert Map.has_key?(updated_pack.files, "auroraborealis")
assert Map.has_key?(updated_pack.files, "baby_in_a_box")
assert Map.has_key?(updated_pack.files, "bear-128")
# Total count should be 4 (all emojis except "bear")
assert updated_pack.files_count == 4
# Clean up the test pack
on_exit(fn ->
File.rm_rf!(test_pack_path)
end)
end
end
test "returns error when zip file is bad", %{pack: pack} do
@ -62,7 +123,7 @@ defmodule Pleroma.Emoji.PackTest do
path: Path.absname("test/instance_static/emoji/test_pack/blank.png")
}
assert Pack.add_file(pack, nil, nil, file) == {:error, :einval}
assert {:error, _} = Pack.add_file(pack, nil, nil, file)
end
test "returns pack when zip file is empty", %{pack: pack} do

View file

@ -9,7 +9,7 @@ defmodule Pleroma.FrontendTest do
@dir "test/frontend_static_test"
setup do
File.mkdir_p!(@dir)
Pleroma.Backports.mkdir_p!(@dir)
clear_config([:instance, :static_dir], @dir)
on_exit(fn ->
@ -46,7 +46,7 @@ defmodule Pleroma.FrontendTest do
folder = Path.join([@dir, "frontends", "pleroma", "fantasy"])
previously_existing = Path.join([folder, "temp"])
File.mkdir_p!(folder)
Pleroma.Backports.mkdir_p!(folder)
File.write!(previously_existing, "yey")
assert File.exists?(previously_existing)

View file

@ -0,0 +1,179 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnTest do
use ExUnit.Case
import Mox
alias Pleroma.Gun.Conn
setup :verify_on_exit!
setup do
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
stub(Pleroma.GunMock, :await_tunnel_up, fn _, _, _ -> {:ok, :http2} end)
stub(Pleroma.GunMock, :close, fn _ -> :ok end)
:ok
end
test "selects the direct transport from the URI scheme" do
expect(Pleroma.GunMock, :open, 2, fn
~c"example.com", 8443, %{transport: :tls, tls_opts: tls_opts} ->
assert tls_opts[:verify] == :verify_peer
{:ok, spawn_link(fn -> Process.sleep(:infinity) end)}
~c"example.com", 443, %{transport: :tcp} ->
{:ok, spawn_link(fn -> Process.sleep(:infinity) end)}
end)
assert {:ok, https_conn, :http, nil} = Conn.open(URI.parse("https://example.com:8443"), [])
assert {:ok, http_conn, :http, nil} = Conn.open(URI.parse("http://example.com:443"), [])
stop_conn(https_conn)
stop_conn(http_conn)
end
test "passes authentication to an HTTP CONNECT proxy" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
stream = make_ref()
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8080, opts ->
assert opts.transport == :tcp
refute Map.has_key?(opts, :tls_opts)
{:ok, conn}
end)
expect(Pleroma.GunMock, :connect, fn ^conn, connect_opts ->
assert connect_opts.username == "alice"
assert connect_opts.password == "secret"
assert connect_opts.transport == :tls
assert connect_opts.protocols == [:http2, :http]
stream
end)
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 200, []} end)
assert {:ok, ^conn, :http2, ^stream} =
Conn.open(URI.parse("https://origin.example/inbox"),
proxy: {~c"proxy.example", 8080},
proxy_auth: {"alice", "secret"}
)
stop_conn(conn)
end
test "uses separate TLS options for a TLS CONNECT proxy" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
stream = make_ref()
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
assert opts.transport == :tls
assert opts.tls_opts == [verify: :verify_none]
{:ok, conn}
end)
expect(Pleroma.GunMock, :connect, fn ^conn, _connect_opts -> stream end)
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 200, []} end)
assert {:ok, ^conn, :http2, ^stream} =
Conn.open(URI.parse("https://origin.example/inbox"),
proxy: {~c"proxy.example", 8443},
transport: :tls,
proxy_tls_opts: [verify: :verify_none]
)
stop_conn(conn)
end
test "uses HTTP/1 for a TLS forwarding proxy" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
assert opts.transport == :tls
assert opts.tls_opts == [verify: :verify_none]
assert opts.protocols == [:http]
{:ok, conn}
end)
assert {:ok, ^conn, :http, :forward_proxy} =
Conn.open(URI.parse("http://origin.example/inbox"),
proxy: {~c"proxy.example", 8443},
transport: :tls,
proxy_tls_opts: [verify: :verify_none]
)
stop_conn(conn)
end
test "passes authentication and remote DNS destination to a SOCKS5 proxy" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 1080, opts ->
assert opts.protocols == [:socks]
assert opts.transport == :tcp
assert opts.socks_opts.host == ~c"origin.example"
assert opts.socks_opts.port == 443
assert opts.socks_opts.auth == [{:username_password, "alice", "secret"}]
assert opts.socks_opts.transport == :tls
assert opts.socks_opts.protocols == [:http2, :http]
refute Map.has_key?(opts, :tls_opts)
{:ok, conn}
end)
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :socks} end)
assert {:ok, ^conn, :http2, nil} =
Conn.open(URI.parse("https://origin.example/inbox"),
proxy: {:socks5, ~c"proxy.example", 1080},
proxy_auth: {"alice", "secret"}
)
stop_conn(conn)
end
test "closes a CONNECT socket when proxy authentication fails" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
stream = make_ref()
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :nofin, 407, []} end)
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
assert {:error, :proxy_auth_failed} =
Conn.open(URI.parse("https://origin.example/inbox"),
proxy: {~c"proxy.example", 8080},
proxy_auth: {"alice", "wrong"}
)
end
test "returns the proxy status when CONNECT is forbidden" do
conn = spawn_link(fn -> Process.sleep(:infinity) end)
stream = make_ref()
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 403, []} end)
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
assert {:error, {:proxy_connect_failed, 403}} =
Conn.open(URI.parse("https://origin.example/inbox"),
proxy: {~c"proxy.example", 8080}
)
end
test "rejects SOCKS4 before opening a socket" do
assert {:error, :socks4_unsupported} =
Conn.open(URI.parse("https://origin.example/inbox"),
proxy: {:socks4, ~c"proxy.example", 1080}
)
end
defp stop_conn(conn) do
Process.unlink(conn)
Process.exit(conn, :kill)
end
end

View file

@ -11,8 +11,11 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
defp gun_mock(_) do
Pleroma.GunMock
|> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(100) end) end)
|> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(:infinity) end) end)
|> stub(:await_up, fn _, _ -> {:ok, :http} end)
|> stub(:connect, fn _, _ -> make_ref() end)
|> stub(:await, fn _, _ -> {:response, :fin, 200, []} end)
|> stub(:await_tunnel_up, fn _, _, _ -> {:ok, :http2} end)
|> stub(:set_owner, fn _, _ -> :ok end)
:ok
@ -20,40 +23,213 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
setup :gun_mock
test "gives the same connection to 2 concurrent requests" do
Enum.map(
[
"http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200530163914.pdf",
"http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200528183427.pdf"
],
fn uri ->
uri = URI.parse(uri)
task_parent = self()
test "opens sibling HTTP/1 connections while an existing connection is in use" do
uri = URI.parse("http://www.korean-books.com.kp/document.pdf")
Task.start_link(fn ->
{:ok, conn} = ConnectionPool.get_conn(uri, [])
ConnectionPool.release_conn(conn)
send(task_parent, conn)
end)
end
)
assert {:ok, first} = ConnectionPool.get_conn(uri, [])
assert {:ok, second} = ConnectionPool.get_conn(uri, [])
assert first != second
[pid, pid] =
for _ <- 1..2 do
receive do
pid -> pid
end
assert :ok = ConnectionPool.release_conn(first)
assert :ok = ConnectionPool.release_conn(second)
end
test "multiplexes concurrent HTTP/2 leases on one connection" do
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
uri = URI.parse("https://h2.example/inbox")
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
assert :ok = ConnectionPool.release_conn(conn)
assert :ok = ConnectionPool.release_conn(conn)
assert Process.alive?(conn)
end
test "does not share connections across direct and authenticated proxy routes" do
uri = URI.parse("https://routes.example/inbox")
assert {:ok, direct} = ConnectionPool.get_conn(uri, [])
assert {:ok, proxied} =
ConnectionPool.get_conn(uri,
proxy: {~c"proxy.example", 8080},
proxy_auth: {"alice", "secret"}
)
assert {:ok, other_credentials} =
ConnectionPool.get_conn(uri,
proxy: {~c"proxy.example", 8080},
proxy_auth: {"bob", "different-secret"}
)
assert Enum.uniq([direct, proxied, other_credentials]) ==
[direct, proxied, other_credentials]
registry_dump = inspect(Registry.select(ConnectionPool, [{{:"$1", :_, :_}, [], [:"$1"]}]))
refute registry_dump =~ "secret"
Enum.each([direct, proxied, other_credentials], &ConnectionPool.release_conn/1)
end
test "coalesces concurrent cold HTTP/2 checkouts behind one connection" do
test_pid = self()
stub(Pleroma.GunMock, :await_up, fn _, _ ->
send(test_pid, {:awaiting_connection, self()})
receive do
:continue -> {:ok, :http2}
end
end)
uri = URI.parse("https://cold-h2.example/inbox")
tasks =
for _ <- 1..10 do
Task.async(fn -> ConnectionPool.get_conn(uri, []) end)
end
assert_receive {:awaiting_connection, worker_pid}
refute_receive {:awaiting_connection, _other_worker}, 50
send(worker_pid, :continue)
assert [{:ok, conn}] = tasks |> Enum.map(&Task.await/1) |> Enum.uniq()
assert Process.alive?(conn)
end
test "shares a cold connection failure with concurrent waiters" do
test_pid = self()
stub(Pleroma.GunMock, :open, fn _, _, _ ->
send(test_pid, {:opening_connection, self()})
receive do
:fail -> {:error, :econnrefused}
end
end)
uri = URI.parse("https://unreachable.example/inbox")
tasks =
for _ <- 1..10 do
Task.async(fn -> ConnectionPool.get_conn(uri, []) end)
end
assert_receive {:opening_connection, worker_pid}
refute_receive {:opening_connection, _other_worker}, 50
send(worker_pid, :fail)
assert Enum.all?(tasks, fn task -> match?({:error, _reason}, Task.await(task)) end)
refute_receive {:opening_connection, _other_worker}, 50
end
test "bounds many-host workloads and closes released idle workers" do
clear_config([:connections_pool, :max_connections]) do
clear_config([:connections_pool, :max_connections], 25)
clear_config([:connections_pool, :max_idle_time], 5)
restart_worker_supervisor()
on_exit(&restart_worker_supervisor/0)
end
connections =
Enum.map(1..25, fn host_number ->
uri = URI.parse("https://host-#{host_number}.example/inbox")
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
conn
end)
assert DynamicSupervisor.count_children(Pleroma.Gun.ConnectionPool.WorkerSupervisor).active ==
25
assert {:error, :pool_full} =
ConnectionPool.get_conn(URI.parse("https://overflow.example/inbox"), [])
Enum.each(connections, &ConnectionPool.release_conn/1)
assert eventually(fn ->
DynamicSupervisor.count_children(Pleroma.Gun.ConnectionPool.WorkerSupervisor).active ==
0
end)
end
test "does not reclaim a worker that became active after idle selection" do
uri = URI.parse("https://reclaim-race.example/inbox")
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
assert :ok = ConnectionPool.release_conn(conn)
worker = worker_for_conn(conn)
assert {:idle, _crf, _last_reference} = GenServer.call(worker, :reclaim_info)
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
assert :in_use = GenServer.call(worker, :reclaim)
assert Process.alive?(worker)
assert :ok = ConnectionPool.release_conn(conn)
end
test "cancels one HTTP/2 stream without discarding the connection" do
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
uri = URI.parse("https://cancel-h2.example/inbox")
stream = make_ref()
expect(Pleroma.GunMock, :cancel, fn conn, ^stream when is_pid(conn) -> :ok end)
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
assert :ok = ConnectionPool.register_stream(conn, stream)
assert :ok = ConnectionPool.cancel_stream(conn, stream)
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
assert :ok = ConnectionPool.release_conn(conn)
end
test "ignores a stale Gun error after cancelling an HTTP/2 stream" do
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
uri = URI.parse("https://stale-cancel-h2.example/inbox")
stream = make_ref()
expect(Pleroma.GunMock, :cancel, fn conn, ^stream when is_pid(conn) -> :ok end)
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
worker = worker_for_conn(conn)
assert :ok = ConnectionPool.register_stream(conn, stream)
assert :ok = ConnectionPool.cancel_stream(conn, stream)
send(worker, {:gun_error, conn, stream, {:badstate, "The stream cannot be found."}})
assert %{clients: %{}} = :sys.get_state(worker)
assert Process.alive?(worker)
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
assert :ok = ConnectionPool.release_conn(conn)
end
test "stops an HTTP/1 worker when a client dies with an active stream" do
uri = URI.parse("http://dead-stream.example/media")
parent = self()
client =
spawn(fn ->
{:ok, conn} = ConnectionPool.get_conn(uri, [])
:ok = ConnectionPool.register_stream(conn, make_ref())
send(parent, {:active_stream, conn})
Process.sleep(:infinity)
end)
assert_receive {:active_stream, conn}
worker = worker_for_conn(conn)
Process.exit(client, :kill)
assert eventually(fn -> not Process.alive?(worker) end)
end
test "connection limit is respected with concurrent requests" do
clear_config([:connections_pool, :max_connections]) do
clear_config([:connections_pool, :max_connections], 1)
# The supervisor needs a reboot to apply the new config setting
Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill)
restart_worker_supervisor()
on_exit(fn ->
Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill)
restart_worker_supervisor()
end)
end
@ -96,4 +272,41 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|> Enum.sort()
end)
end
defp restart_worker_supervisor do
supervisor = Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor)
monitor = Process.monitor(supervisor)
Process.exit(supervisor, :kill)
assert_receive {:DOWN, ^monitor, :process, ^supervisor, :killed}
assert eventually(fn ->
case Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor) do
pid when is_pid(pid) -> pid != supervisor and Process.alive?(pid)
nil -> false
end
end)
end
defp worker_for_conn(conn) do
[worker] =
Registry.select(ConnectionPool, [
{{:_, :"$1", {:"$2", :_}}, [{:==, :"$2", conn}], [:"$1"]}
])
worker
end
defp eventually(predicate, attempts \\ 50)
defp eventually(_predicate, 0), do: false
defp eventually(predicate, attempts) do
if predicate.() do
true
else
Process.sleep(10)
eventually(predicate, attempts - 1)
end
end
end

View file

@ -14,4 +14,133 @@ defmodule Pleroma.HashtagTest do
assert {:name, {"can't be blank", [validation: :required]}} in changeset.errors
end
end
describe "search_hashtags" do
test "searches hashtags by partial match" do
{:ok, _} = Hashtag.get_or_create_by_name("car")
{:ok, _} = Hashtag.get_or_create_by_name("racecar")
{:ok, _} = Hashtag.get_or_create_by_name("nascar")
{:ok, _} = Hashtag.get_or_create_by_name("bicycle")
results = Hashtag.search("car")
assert "car" in results
assert "racecar" in results
assert "nascar" in results
refute "bicycle" in results
results = Hashtag.search("race")
assert "racecar" in results
refute "car" in results
refute "nascar" in results
refute "bicycle" in results
results = Hashtag.search("nonexistent")
assert results == []
end
test "searches hashtags by multiple words in query" do
{:ok, _} = Hashtag.get_or_create_by_name("computer")
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
{:ok, _} = Hashtag.get_or_create_by_name("desktop")
{:ok, _} = Hashtag.get_or_create_by_name("phone")
# Search for "new computer" - should return "computer"
results = Hashtag.search("new computer")
assert "computer" in results
refute "laptop" in results
refute "desktop" in results
refute "phone" in results
# Search for "computer laptop" - should return both
results = Hashtag.search("computer laptop")
assert "computer" in results
assert "laptop" in results
refute "desktop" in results
refute "phone" in results
# Search for "new phone" - should return "phone"
results = Hashtag.search("new phone")
assert "phone" in results
refute "computer" in results
refute "laptop" in results
refute "desktop" in results
end
test "supports pagination" do
{:ok, _} = Hashtag.get_or_create_by_name("alpha")
{:ok, _} = Hashtag.get_or_create_by_name("beta")
{:ok, _} = Hashtag.get_or_create_by_name("gamma")
{:ok, _} = Hashtag.get_or_create_by_name("delta")
results = Hashtag.search("a", limit: 2)
assert length(results) == 2
results = Hashtag.search("a", limit: 2, offset: 1)
assert length(results) == 2
end
test "handles matching many search terms" do
{:ok, _} = Hashtag.get_or_create_by_name("computer")
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
{:ok, _} = Hashtag.get_or_create_by_name("phone")
{:ok, _} = Hashtag.get_or_create_by_name("tablet")
results = Hashtag.search("new fast computer laptop phone tablet device")
assert "computer" in results
assert "laptop" in results
assert "phone" in results
assert "tablet" in results
end
test "ranks results by match quality" do
{:ok, _} = Hashtag.get_or_create_by_name("my_computer")
{:ok, _} = Hashtag.get_or_create_by_name("computer_science")
{:ok, _} = Hashtag.get_or_create_by_name("computer")
results = Hashtag.search("computer")
# Exact match first
assert Enum.at(results, 0) == "computer"
# Prefix match would be next
assert Enum.at(results, 1) == "computer_science"
# worst match is last
assert Enum.at(results, 2) == "my_computer"
end
test "prioritizes shorter names when ranking is equal" do
# Create hashtags with same ranking but different lengths
{:ok, _} = Hashtag.get_or_create_by_name("car")
{:ok, _} = Hashtag.get_or_create_by_name("racecar")
{:ok, _} = Hashtag.get_or_create_by_name("nascar")
# Search for "car" - shorter names should come first
results = Hashtag.search("car")
# Shortest exact match first
assert Enum.at(results, 0) == "car"
assert "racecar" in results
assert "nascar" in results
end
test "handles hashtag symbols in search query" do
{:ok, _} = Hashtag.get_or_create_by_name("computer")
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
{:ok, _} = Hashtag.get_or_create_by_name("phone")
results_with_hash = Hashtag.search("#computer #laptop")
results_without_hash = Hashtag.search("computer laptop")
assert results_with_hash == results_without_hash
results_mixed = Hashtag.search("#computer laptop #phone")
assert "computer" in results_mixed
assert "laptop" in results_mixed
assert "phone" in results_mixed
results_only_hash = Hashtag.search("#computer")
results_no_hash = Hashtag.search("computer")
assert results_only_hash == results_no_hash
end
end
end

View file

@ -73,5 +73,11 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do
assert opts[:proxy] == {~c"example.com", 4321}
end
test "uses managed chunks for streamed responses" do
opts = Gun.options([stream: true], URI.parse("https://example.com"))
assert opts[:body_as] == :chunks
end
end
end

View file

@ -16,6 +16,14 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
describe "options/2" do
setup do: clear_config([:http, :adapter], a: 1, b: 2)
test "uses redirect-safe defaults", %{uri: uri} do
opts = Hackney.options([], uri)
assert opts[:follow_redirect] == false
assert opts[:force_redirect] == false
assert opts[:with_body] == true
end
test "add proxy and opts from config", %{uri: uri} do
opts = Hackney.options([proxy: "localhost:8123"], uri)

View file

@ -0,0 +1,808 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.GunIntegrationTest do
use ExUnit.Case, async: false
use Pleroma.Tests.Helpers
alias Pleroma.Tesla.Middleware.ConnectionPool
setup do
clear_config([Pleroma.Gun], Pleroma.Gun.API)
clear_config([:connections_pool, :max_idle_time], 1_000)
origin = start_tls_origin(self())
connect_proxy = start_connect_proxy(self())
forward_proxy = start_forward_proxy(self())
socks_proxy = start_socks5_proxy(self())
on_exit(fn ->
Process.sleep(20)
stop_server(socks_proxy)
stop_server(forward_proxy)
stop_server(connect_proxy)
stop_server(origin)
end)
{:ok,
origin: origin,
connect_proxy: connect_proxy,
forward_proxy: forward_proxy,
socks_proxy: socks_proxy}
end
test "requests HTTPS directly on a non-standard port", %{origin: origin} do
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
end
test "follows redirects through an authenticated CONNECT proxy", %{
origin: origin,
connect_proxy: proxy
} do
proxy_opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
request(origin.base_url <> "/redirect", proxy_opts, true)
expected = "Basic " <> Base.encode64("alice:secret")
assert_receive {:connect_proxy, ^expected, "127.0.0.1", origin_port}
assert origin_port == origin.port
end
test "requests through an authenticated CONNECT proxy", %{
origin: origin,
connect_proxy: proxy
} do
proxy_opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
result = request(origin.base_url <> "/final", proxy_opts)
expected = "Basic " <> Base.encode64("alice:secret")
assert_receive {:connect_proxy, ^expected, "127.0.0.1", origin_port}
assert origin_port == origin.port
assert worker_protocol(origin.port) == :http
assert_receive {:origin_request, "/final"}
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = result
end
test "requests HTTP through a forwarding proxy", %{forward_proxy: proxy} do
proxy_opts = [proxy: {~c"127.0.0.1", proxy.port}]
url = "http://origin.example:8080/final?key=value"
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
assert_receive {:forward_proxy, ^url, "origin.example:8080", nil}
end
test "authenticates HTTP requests to a forwarding proxy", %{forward_proxy: proxy} do
proxy_opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
url = "http://origin.example/final"
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
expected = "Basic " <> Base.encode64("alice:secret")
assert_receive {:forward_proxy, ^url, "origin.example", ^expected}
end
test "opens a request-ready authenticated CONNECT tunnel", %{
origin: origin,
connect_proxy: proxy
} do
uri = URI.parse(origin.base_url <> "/final")
opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"},
tls_opts: [verify: :verify_none],
connect_timeout: 2_000
]
assert {:ok, conn, :http, tunnel} = Pleroma.Gun.Conn.open(uri, opts)
stream = :gun.get(conn, "/final", [], %{tunnel: tunnel})
assert {:response, :nofin, 200, _headers} = :gun.await(conn, stream, 2_000)
assert {:ok, "ok"} = :gun.await_body(conn, stream, 2_000)
:gun.close(conn)
end
test "streams chunks through an authenticated CONNECT tunnel", %{
origin: origin,
connect_proxy: proxy
} do
proxy_opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
assert {:ok, %Tesla.Env{status: 200, body: client}} =
request_chunks(origin.base_url <> "/chunks", proxy_opts)
assert collect_chunks(client) == "hello"
assert :ok = Pleroma.Gun.ConnectionPool.release_conn(client.pid)
end
test "discards an unfinished HTTP/1 redirect stream before following it", %{
origin: origin,
connect_proxy: proxy
} do
proxy_opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
assert {:ok, %Tesla.Env{status: 200, body: client}} =
request_chunks(origin.base_url <> "/redirect_chunks", proxy_opts, true)
assert collect_chunks(client) == "hello"
assert :ok = Pleroma.Gun.ConnectionPool.release_conn(client.pid)
expected = "Basic " <> Base.encode64("alice:secret")
assert_receive {:connect_proxy, ^expected, "127.0.0.1", _port}
assert_receive {:connect_proxy, ^expected, "127.0.0.1", _port}
end
test "uploads multipart bodies through an authenticated CONNECT tunnel", %{
origin: origin,
connect_proxy: proxy
} do
proxy_opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
multipart = Tesla.Multipart.new() |> Tesla.Multipart.add_field("name", "value")
assert {:ok, %Tesla.Env{status: 200}} =
request_multipart(origin.base_url <> "/upload", multipart, proxy_opts)
assert_receive {:origin_headers, "/upload", headers}
assert String.downcase(headers) =~ "content-type: multipart/form-data;"
assert_receive {:origin_body, "/upload", body}
assert body =~ "name=\"name\""
assert body =~ "value"
end
test "uploads streamed bodies through an authenticated CONNECT tunnel", %{
origin: origin,
connect_proxy: proxy
} do
proxy_opts = [
proxy: {~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
body = Stream.map(["streamed", "-body"], & &1)
assert {:ok, %Tesla.Env{status: 200}} =
request_stream(origin.base_url <> "/upload", body, proxy_opts)
assert_receive {:origin_body, "/upload", "streamed-body"}
end
test "discards a partial HTTP/1 upload when body enumeration raises", %{origin: origin} do
body =
Stream.map(["partial", "raise"], fn
"raise" -> raise "upload failed"
part -> part
end)
assert_raise RuntimeError, "upload failed", fn ->
request_stream(origin.base_url <> "/upload", body, [])
end
assert_receive {:origin_headers, "/upload", _headers}
assert worker_protocol(origin.port) == nil
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
end
test "releases the lease when Gun rejects an invalid request header", %{origin: origin} do
adapter_opts = [
tls_opts: [verify: :verify_none],
connect_timeout: 2_000,
timeout: 2_000
]
client = Tesla.client([ConnectionPool], Tesla.Adapter.Gun)
assert {:invalid_request_header, "x-test", _message} =
catch_error(
Tesla.get(client, origin.base_url <> "/final",
headers: [{"x-test", "value\r\nx-injected: true"}],
opts: [adapter: adapter_opts]
)
)
assert worker_clients(origin.port) == %{}
refute_receive {:origin_request, "/final"}
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
end
test "requests through authenticated SOCKS5 with proxy-side DNS", %{
origin: origin,
socks_proxy: proxy
} do
proxy_opts = [
proxy: {:socks5, ~c"127.0.0.1", proxy.port},
proxy_auth: {"alice", "secret"}
]
url = "https://localhost:#{origin.port}/final"
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
assert_receive {:socks5_proxy, "alice", "secret", "localhost", origin_port}
assert origin_port == origin.port
end
test "requests through unauthenticated SOCKS5 with an IP proxy host", %{
origin: origin,
socks_proxy: proxy
} do
proxy_opts = [proxy: {:socks5, {127, 0, 0, 1}, proxy.port}]
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
request(origin.base_url <> "/final", proxy_opts)
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
assert origin_port == origin.port
end
test "requests through unauthenticated SOCKS5 with a localhost proxy host", %{
origin: origin,
socks_proxy: proxy
} do
proxy_opts = [proxy: {:socks5, ~c"localhost", proxy.port}]
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
request(origin.base_url <> "/final", proxy_opts)
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
assert origin_port == origin.port
end
defp request(url, proxy_opts \\ [], follow_redirects \\ false) do
middleware =
if follow_redirects do
[Tesla.Middleware.FollowRedirects, ConnectionPool]
else
[ConnectionPool]
end
adapter_opts =
Keyword.merge(
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
proxy_opts
)
middleware
|> Tesla.client(Tesla.Adapter.Gun)
|> Tesla.get(url, opts: [adapter: adapter_opts])
end
defp request_chunks(url, proxy_opts, follow_redirects \\ false) do
adapter_opts =
Keyword.merge(
[
body_as: :chunks,
tls_opts: [verify: :verify_none],
connect_timeout: 2_000,
timeout: 2_000
],
proxy_opts
)
middleware =
if follow_redirects do
[Tesla.Middleware.FollowRedirects, ConnectionPool]
else
[ConnectionPool]
end
middleware
|> Tesla.client(Tesla.Adapter.Gun)
|> Tesla.get(url, opts: [adapter: adapter_opts])
end
defp request_multipart(url, multipart, proxy_opts) do
adapter_opts =
Keyword.merge(
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
proxy_opts
)
[ConnectionPool]
|> Tesla.client(Tesla.Adapter.Gun)
|> Tesla.post(url, multipart, opts: [adapter: adapter_opts])
end
defp request_stream(url, body, proxy_opts) do
adapter_opts =
Keyword.merge(
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
proxy_opts
)
[ConnectionPool]
|> Tesla.client(Tesla.Adapter.Gun)
|> Tesla.post(url, body, opts: [adapter: adapter_opts])
end
defp collect_chunks(client, acc \\ "") do
case Tesla.Adapter.Gun.read_chunk(client.pid, client.stream, client.opts) do
{:nofin, chunk} -> collect_chunks(client, acc <> chunk)
{:fin, chunk} -> acc <> chunk
end
end
defp start_tls_origin(parent) do
certfile = Path.expand("../../fixtures/server.pem", __DIR__)
keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
{:ok, listener} =
:ssl.listen(0, [
:binary,
certfile: certfile,
keyfile: keyfile,
reuseaddr: true,
active: false,
packet: :raw,
ip: {127, 0, 0, 1}
])
{:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
{:ok, acceptor} = Task.start_link(fn -> accept_tls_loop(listener, parent) end)
%{
listener: listener,
acceptor: acceptor,
transport: :ssl,
port: port,
base_url: "https://127.0.0.1:#{port}"
}
end
defp accept_tls_loop(listener, parent) do
case :ssl.transport_accept(listener) do
{:ok, tcp_socket} ->
case :ssl.handshake(tcp_socket, 2_000) do
{:ok, socket} ->
pid = spawn(fn -> receive_and_serve_tls(socket, parent) end)
:ok = :ssl.controlling_process(socket, pid)
send(pid, :serve)
{:error, _reason} ->
:gen_tcp.close(tcp_socket)
end
accept_tls_loop(listener, parent)
{:error, _reason} ->
:ok
end
end
defp receive_and_serve_tls(socket, parent) do
receive do
:serve -> serve_tls(socket, parent)
end
end
defp serve_tls(socket, parent) do
case recv_headers(:ssl, socket) do
{:ok, request} ->
{headers, buffered_body} = split_headers(request)
path = request_path(headers)
send(parent, {:origin_request, path})
send(parent, {:origin_headers, path, headers})
case path do
"/redirect" ->
send_tls_response(socket, 302, "Found", [{"Location", "/final"}], "")
"/redirect_chunks" ->
send_tls_response(socket, 302, "Found", [{"Location", "/chunks"}], "discard-me")
"/final" ->
send_tls_response(socket, 200, "OK", [], "ok")
"/chunks" ->
send_tls_chunks(socket, ["hel", "lo"])
"/upload" ->
{:ok, body} = recv_request_body(socket, headers, buffered_body)
send(parent, {:origin_body, path, body})
send_tls_response(socket, 200, "OK", [], "uploaded")
_ ->
send_tls_response(socket, 404, "Not Found", [], "not found")
end
serve_tls(socket, parent)
{:error, _reason} ->
:ssl.close(socket)
end
end
defp send_tls_response(socket, status, reason, headers, body) do
headers =
[
{"Content-Length", Integer.to_string(byte_size(body))},
{"Connection", "keep-alive"}
] ++ headers
:ssl.send(socket, [
"HTTP/1.1 ",
Integer.to_string(status),
" ",
reason,
"\r\n",
Enum.map(headers, fn {key, value} -> [key, ": ", value, "\r\n"] end),
"\r\n",
body
])
end
defp send_tls_chunks(socket, chunks) do
:ssl.send(socket, [
"HTTP/1.1 200 OK\r\n",
"Transfer-Encoding: chunked\r\n",
"Connection: keep-alive\r\n\r\n",
Enum.map(chunks, fn chunk ->
[Integer.to_string(byte_size(chunk), 16), "\r\n", chunk, "\r\n"]
end),
"0\r\n\r\n"
])
end
defp start_connect_proxy(parent) do
start_tcp_server(fn socket -> serve_connect_proxy(socket, parent) end)
end
defp serve_connect_proxy(socket, parent) do
with {:ok, headers} <- recv_headers(:gen_tcp, socket),
{:ok, host, port} <- parse_connect(headers),
auth when is_binary(auth) <- header(headers, "proxy-authorization"),
{:ok, upstream} <- tcp_connect(host, port) do
send(parent, {:connect_proxy, auth, host, port})
:ok = :gen_tcp.send(socket, "HTTP/1.1 200 Connection established\r\n\r\n")
tunnel(socket, upstream)
else
_ ->
:gen_tcp.send(
socket,
"HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n"
)
:gen_tcp.close(socket)
end
end
defp start_forward_proxy(parent) do
start_tcp_server(fn socket -> serve_forward_proxy(socket, parent) end)
end
defp serve_forward_proxy(socket, parent) do
with {:ok, request} <- recv_headers(:gen_tcp, socket) do
{headers, _buffered_body} = split_headers(request)
target = request_path(headers)
host = header(headers, "host")
auth = header(headers, "proxy-authorization")
if String.starts_with?(target, "http://") do
send(parent, {:forward_proxy, target, host, auth})
:gen_tcp.send(
socket,
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok"
)
serve_forward_proxy(socket, parent)
else
:gen_tcp.send(
socket,
"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
)
:gen_tcp.close(socket)
end
end
end
defp start_socks5_proxy(parent) do
start_tcp_server(fn socket -> serve_socks5_proxy(socket, parent) end)
end
defp serve_socks5_proxy(socket, parent) do
with {:ok, <<5, method_count>>} <- :gen_tcp.recv(socket, 2, 2_000),
{:ok, methods} <- :gen_tcp.recv(socket, method_count, 2_000),
{:ok, username, password} <- negotiate_socks5_auth(socket, methods),
{:ok, host, port} <- recv_socks5_destination(socket),
{:ok, upstream} <- tcp_connect(host, port),
:ok <- :gen_tcp.send(socket, <<5, 0, 0, 1, 0, 0, 0, 0, 0, 0>>) do
send(parent, {:socks5_proxy, username, password, host, port})
tunnel(socket, upstream)
else
_ ->
:gen_tcp.close(socket)
end
end
defp negotiate_socks5_auth(socket, methods) do
cond do
:binary.match(methods, <<2>>) != :nomatch ->
with :ok <- :gen_tcp.send(socket, <<5, 2>>),
{:ok, username, password} <- recv_socks5_auth(socket),
:ok <- :gen_tcp.send(socket, <<1, 0>>) do
{:ok, username, password}
end
:binary.match(methods, <<0>>) != :nomatch ->
with :ok <- :gen_tcp.send(socket, <<5, 0>>) do
{:ok, nil, nil}
end
true ->
{:error, :unsupported_auth}
end
end
defp recv_socks5_auth(socket) do
with {:ok, <<1, username_length>>} <- :gen_tcp.recv(socket, 2, 2_000),
{:ok, username} <- :gen_tcp.recv(socket, username_length, 2_000),
{:ok, <<password_length>>} <- :gen_tcp.recv(socket, 1, 2_000),
{:ok, password} <- :gen_tcp.recv(socket, password_length, 2_000) do
{:ok, username, password}
end
end
defp recv_socks5_destination(socket) do
with {:ok, <<5, 1, 0, address_type>>} <- :gen_tcp.recv(socket, 4, 2_000),
{:ok, host} <- recv_socks5_host(socket, address_type),
{:ok, <<port::16>>} <- :gen_tcp.recv(socket, 2, 2_000) do
{:ok, host, port}
end
end
defp recv_socks5_host(socket, 1) do
with {:ok, <<a, b, c, d>>} <- :gen_tcp.recv(socket, 4, 2_000) do
{:ok, Enum.join([a, b, c, d], ".")}
end
end
defp recv_socks5_host(socket, 3) do
with {:ok, <<length>>} <- :gen_tcp.recv(socket, 1, 2_000),
{:ok, host} <- :gen_tcp.recv(socket, length, 2_000) do
{:ok, host}
end
end
defp start_tcp_server(handler) do
{:ok, listener} =
:gen_tcp.listen(0, [
:binary,
active: false,
packet: :raw,
reuseaddr: true,
ip: {127, 0, 0, 1}
])
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
{:ok, acceptor} = Task.start_link(fn -> accept_tcp_loop(listener, handler) end)
%{listener: listener, acceptor: acceptor, transport: :gen_tcp, port: port}
end
defp accept_tcp_loop(listener, handler) do
case :gen_tcp.accept(listener) do
{:ok, socket} ->
pid = spawn(fn -> receive_and_serve_tcp(socket, handler) end)
:ok = :gen_tcp.controlling_process(socket, pid)
send(pid, :serve)
accept_tcp_loop(listener, handler)
{:error, _reason} ->
:ok
end
end
defp receive_and_serve_tcp(socket, handler) do
receive do
:serve -> handler.(socket)
end
end
defp recv_headers(transport, socket, acc \\ <<>>) do
case transport.recv(socket, 0, 2_000) do
{:ok, data} ->
acc = acc <> data
if :binary.match(acc, "\r\n\r\n") == :nomatch do
recv_headers(transport, socket, acc)
else
{:ok, acc}
end
{:error, _reason} = error ->
error
end
end
defp request_path(headers) do
headers
|> String.split("\r\n", parts: 2)
|> hd()
|> String.split(" ")
|> Enum.at(1)
end
defp split_headers(request) do
{index, 4} = :binary.match(request, "\r\n\r\n")
split_at = index + 4
<<headers::binary-size(split_at), body::binary>> = request
{headers, body}
end
defp recv_request_body(socket, headers, buffered_body) do
cond do
String.contains?(String.downcase(headers), "transfer-encoding: chunked") ->
recv_chunked_body(socket, buffered_body, "")
content_length = header(headers, "content-length") ->
recv_exact(socket, buffered_body, String.to_integer(content_length))
true ->
{:ok, buffered_body}
end
end
defp recv_chunked_body(socket, buffer, acc) do
with {:ok, line, buffer} <- recv_line(socket, buffer),
{size, ""} <- Integer.parse(line, 16) do
if size == 0 do
{:ok, acc}
else
with {:ok, chunk_and_crlf, buffer} <- take_bytes(socket, buffer, size + 2),
<<chunk::binary-size(size), "\r\n">> <- chunk_and_crlf do
recv_chunked_body(socket, buffer, acc <> chunk)
end
end
end
end
defp recv_line(socket, buffer) do
case :binary.match(buffer, "\r\n") do
{index, 2} ->
<<line::binary-size(index), "\r\n", rest::binary>> = buffer
{:ok, line, rest}
:nomatch ->
with {:ok, data} <- :ssl.recv(socket, 0, 2_000) do
recv_line(socket, buffer <> data)
end
end
end
defp take_bytes(_socket, buffer, size) when byte_size(buffer) >= size do
<<bytes::binary-size(size), rest::binary>> = buffer
{:ok, bytes, rest}
end
defp take_bytes(socket, buffer, size) do
with {:ok, data} <- :ssl.recv(socket, 0, 2_000) do
take_bytes(socket, buffer <> data, size)
end
end
defp recv_exact(socket, buffer, size) do
with {:ok, body, _rest} <- take_bytes(socket, buffer, size) do
{:ok, body}
end
end
defp parse_connect(headers) do
case headers |> String.split("\r\n", parts: 2) |> hd() |> String.split(" ") do
["CONNECT", authority, _protocol] ->
case String.split(authority, ":", parts: 2) do
[host, port] -> {:ok, host, String.to_integer(port)}
_ -> {:error, :invalid_authority}
end
_ ->
{:error, :invalid_connect}
end
end
defp header(headers, wanted_name) do
headers
|> String.split("\r\n")
|> Enum.find_value(fn line ->
case String.split(line, ":", parts: 2) do
[name, value] ->
if String.downcase(name) == wanted_name, do: String.trim(value)
_ ->
nil
end
end)
end
defp tcp_connect(host, port) do
:gen_tcp.connect(
String.to_charlist(host),
port,
[:binary, active: false, packet: :raw],
2_000
)
end
defp worker_protocol(port) do
case worker_state(port) do
nil -> nil
state -> state.protocol
end
end
defp worker_clients(port) do
case worker_state(port) do
nil -> nil
state -> state.clients
end
end
defp worker_state(port) do
prefix = "https:127.0.0.1:#{port}:"
Pleroma.Gun.ConnectionPool
|> Registry.select([{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}])
|> Enum.find_value(fn {key, worker} ->
if String.starts_with?(key, prefix), do: :sys.get_state(worker)
end)
end
defp tunnel(client, upstream) do
:ok = :inet.setopts(client, active: :once)
:ok = :inet.setopts(upstream, active: :once)
tunnel_loop(client, upstream)
end
defp tunnel_loop(client, upstream) do
receive do
{:tcp, ^client, data} ->
:ok = :gen_tcp.send(upstream, data)
:ok = :inet.setopts(client, active: :once)
tunnel_loop(client, upstream)
{:tcp, ^upstream, data} ->
:ok = :gen_tcp.send(client, data)
:ok = :inet.setopts(upstream, active: :once)
tunnel_loop(client, upstream)
{:tcp_closed, _socket} ->
:gen_tcp.close(client)
:gen_tcp.close(upstream)
{:tcp_error, _socket, _reason} ->
:gen_tcp.close(client)
:gen_tcp.close(upstream)
after
10_000 ->
:gen_tcp.close(client)
:gen_tcp.close(upstream)
end
end
defp stop_server(%{listener: listener, acceptor: acceptor, transport: transport}) do
transport.close(listener)
if Process.alive?(acceptor), do: Process.exit(acceptor, :normal)
end
end

View file

@ -0,0 +1,404 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
use ExUnit.Case, async: false
setup do
{:ok, _} = Application.ensure_all_started(:hackney)
{:ok, tls_server} = start_tls_redirect_server()
{:ok, proxy} = start_connect_proxy()
on_exit(fn ->
stop_connect_proxy(proxy)
stop_tls_redirect_server(tls_server)
end)
{:ok, tls_server: tls_server, proxy: proxy}
end
test "hackney follow_redirect crashes behind CONNECT proxy on relative redirects", %{
tls_server: tls_server,
proxy: proxy
} do
url = "#{tls_server.base_url}/redirect"
opts = [
pool: :media,
proxy: proxy.proxy_url,
insecure: true,
connect_timeout: 1_000,
recv_timeout: 1_000,
follow_redirect: true,
force_redirect: true
]
{pid, ref} = spawn_monitor(fn -> :hackney.request(:get, url, [], <<>>, opts) end)
assert_receive {:DOWN, ^ref, :process, ^pid, reason}, 5_000
assert match?({%FunctionClauseError{}, _}, reason) or match?(%FunctionClauseError{}, reason) or
match?({:function_clause, _}, reason)
end
test "redirects work via proxy when hackney follow_redirect is disabled", %{
tls_server: tls_server,
proxy: proxy
} do
url = "#{tls_server.base_url}/redirect"
adapter_opts = [
pool: :media,
proxy: proxy.proxy_url,
insecure: true,
connect_timeout: 1_000,
recv_timeout: 1_000,
follow_redirect: false,
force_redirect: false,
with_body: true
]
client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
end
test "reverse proxy hackney client follows redirects via proxy without crashing", %{
tls_server: tls_server,
proxy: proxy
} do
url = "#{tls_server.base_url}/redirect"
opts = [
pool: :media,
proxy: proxy.proxy_url,
insecure: true,
connect_timeout: 1_000,
recv_timeout: 1_000,
follow_redirect: true
]
assert {:ok, 200, _headers, ref} =
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
assert collect_body(ref) == "ok"
Pleroma.ReverseProxy.Client.Hackney.close(ref)
end
test "hackney reverse proxy follows nested redirects via proxy", %{
tls_server: tls_server,
proxy: proxy
} do
url = "#{tls_server.base_url}/nested_redirect"
opts = [
pool: :media,
proxy: proxy.proxy_url,
insecure: true,
connect_timeout: 1_000,
recv_timeout: 1_000,
follow_redirect: true
]
assert {:ok, 200, _headers, ref} =
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
assert collect_body(ref) == "ok"
Pleroma.ReverseProxy.Client.Hackney.close(ref)
end
test "hackney reverse proxy stops following redirects after limit is reached", %{
tls_server: tls_server,
proxy: proxy
} do
url = "#{tls_server.base_url}/infinite_redirect"
opts = [
pool: :media,
proxy: proxy.proxy_url,
insecure: true,
connect_timeout: 1_000,
recv_timeout: 1_000,
follow_redirect: true
]
assert {:ok, 302, _headers, ref} =
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
Pleroma.ReverseProxy.Client.Hackney.close(ref)
end
defp collect_body(ref, acc \\ "") do
case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do
:done -> acc
{:ok, data, _ref} -> collect_body(ref, acc <> data)
{:error, error} -> flunk("stream_body failed: #{inspect(error)}")
end
end
defp start_tls_redirect_server do
certfile = Path.expand("../../fixtures/server.pem", __DIR__)
keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
{:ok, listener} =
:ssl.listen(0, [
:binary,
certfile: certfile,
keyfile: keyfile,
reuseaddr: true,
active: false,
packet: :raw,
ip: {127, 0, 0, 1}
])
{:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
{:ok, acceptor} =
Task.start_link(fn ->
accept_tls_loop(listener)
end)
{:ok, %{listener: listener, acceptor: acceptor, base_url: "https://127.0.0.1:#{port}"}}
end
defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do
:ok = :ssl.close(listener)
if Process.alive?(acceptor) do
Process.exit(acceptor, :normal)
end
end
defp accept_tls_loop(listener) do
case :ssl.transport_accept(listener) do
{:ok, socket} ->
_ = Task.start(fn -> serve_tls(socket) end)
accept_tls_loop(listener)
{:error, :closed} ->
:ok
{:error, _reason} ->
:ok
end
end
defp serve_tls(tcp_socket) do
with {:ok, ssl_socket} <- :ssl.handshake(tcp_socket, 2_000),
{:ok, data} <- recv_ssl_headers(ssl_socket),
{:ok, path} <- parse_path(data) do
case path do
"/infinite_redirect" ->
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/infinite_redirect"}], "")
"/nested_redirect" ->
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/redirect"}], "")
"/redirect" ->
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "")
"/final" ->
send_ssl_response(ssl_socket, 200, "OK", [], "ok")
_ ->
send_ssl_response(ssl_socket, 404, "Not Found", [], "not found")
end
:ssl.close(ssl_socket)
else
_ ->
_ = :gen_tcp.close(tcp_socket)
:ok
end
end
defp recv_ssl_headers(socket, acc \\ <<>>) do
case :ssl.recv(socket, 0, 1_000) do
{:ok, data} ->
acc = acc <> data
if :binary.match(acc, "\r\n\r\n") != :nomatch do
{:ok, acc}
else
if byte_size(acc) > 8_192 do
{:error, :too_large}
else
recv_ssl_headers(socket, acc)
end
end
{:error, _} = error ->
error
end
end
defp send_ssl_response(socket, status, reason, headers, body) do
base_headers =
[
{"Content-Length", Integer.to_string(byte_size(body))},
{"Connection", "close"}
] ++ headers
iodata =
[
"HTTP/1.1 ",
Integer.to_string(status),
" ",
reason,
"\r\n",
Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
"\r\n",
body
]
:ssl.send(socket, iodata)
end
defp start_connect_proxy do
{:ok, listener} =
:gen_tcp.listen(0, [
:binary,
active: false,
packet: :raw,
reuseaddr: true,
ip: {127, 0, 0, 1}
])
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
{:ok, acceptor} =
Task.start_link(fn ->
accept_proxy_loop(listener)
end)
{:ok, %{listener: listener, acceptor: acceptor, proxy_url: "127.0.0.1:#{port}"}}
end
defp stop_connect_proxy(%{listener: listener, acceptor: acceptor}) do
:ok = :gen_tcp.close(listener)
if Process.alive?(acceptor) do
Process.exit(acceptor, :normal)
end
end
defp accept_proxy_loop(listener) do
case :gen_tcp.accept(listener) do
{:ok, socket} ->
_ = Task.start(fn -> serve_proxy(socket) end)
accept_proxy_loop(listener)
{:error, :closed} ->
:ok
{:error, _reason} ->
:ok
end
end
defp serve_proxy(client_socket) do
with {:ok, {headers, rest}} <- recv_tcp_headers(client_socket),
{:ok, {host, port}} <- parse_connect(headers),
{:ok, upstream_socket} <- connect_upstream(host, port) do
:gen_tcp.send(client_socket, "HTTP/1.1 200 Connection established\r\n\r\n")
if rest != <<>> do
:gen_tcp.send(upstream_socket, rest)
end
tunnel(client_socket, upstream_socket)
else
_ ->
:gen_tcp.close(client_socket)
:ok
end
end
defp tunnel(client_socket, upstream_socket) do
parent = self()
_ = spawn_link(fn -> forward(client_socket, upstream_socket, parent) end)
_ = spawn_link(fn -> forward(upstream_socket, client_socket, parent) end)
receive do
:tunnel_closed -> :ok
after
10_000 -> :ok
end
:gen_tcp.close(client_socket)
:gen_tcp.close(upstream_socket)
end
defp forward(from_socket, to_socket, parent) do
case :gen_tcp.recv(from_socket, 0, 10_000) do
{:ok, data} ->
_ = :gen_tcp.send(to_socket, data)
forward(from_socket, to_socket, parent)
{:error, _reason} ->
send(parent, :tunnel_closed)
:ok
end
end
defp recv_tcp_headers(socket, acc \\ <<>>) do
case :gen_tcp.recv(socket, 0, 1_000) do
{:ok, data} ->
acc = acc <> data
case :binary.match(acc, "\r\n\r\n") do
:nomatch ->
if byte_size(acc) > 8_192 do
{:error, :too_large}
else
recv_tcp_headers(socket, acc)
end
{idx, _len} ->
split_at = idx + 4
<<headers::binary-size(split_at), rest::binary>> = acc
{:ok, {headers, rest}}
end
{:error, _} = error ->
error
end
end
defp parse_connect(data) do
with [request_line | _] <- String.split(data, "\r\n", trim: true),
["CONNECT", hostport | _] <- String.split(request_line, " ", parts: 3),
[host, port_str] <- String.split(hostport, ":", parts: 2),
{port, ""} <- Integer.parse(port_str) do
{:ok, {host, port}}
else
_ -> {:error, :invalid_connect}
end
end
defp connect_upstream(host, port) do
address =
case :inet.parse_address(String.to_charlist(host)) do
{:ok, ip} -> ip
{:error, _} -> String.to_charlist(host)
end
:gen_tcp.connect(address, port, [:binary, active: false, packet: :raw], 1_000)
end
defp parse_path(data) do
case String.split(data, "\r\n", parts: 2) do
[request_line | _] ->
case String.split(request_line, " ") do
[_method, path, _protocol] -> {:ok, path}
_ -> {:error, :invalid_request}
end
_ ->
{:error, :invalid_request}
end
end
end

View file

@ -0,0 +1,151 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.HackneyRedirectRegressionTest do
use ExUnit.Case, async: false
alias Pleroma.HTTP.AdapterHelper.Hackney, as: HackneyAdapterHelper
setup do
{:ok, _} = Application.ensure_all_started(:hackney)
{:ok, server} = start_server()
on_exit(fn -> stop_server(server) end)
{:ok, server: server}
end
test "pooled redirects work with follow_redirect disabled", %{server: server} do
url = "#{server.base_url}/redirect"
uri = URI.parse(url)
adapter_opts =
HackneyAdapterHelper.options(
[pool: :media, follow_redirect: false, no_proxy_env: true],
uri
)
client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
end
defp start_server do
{:ok, listener} =
:gen_tcp.listen(0, [
:binary,
active: false,
packet: :raw,
reuseaddr: true,
ip: {127, 0, 0, 1}
])
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
{:ok, acceptor} =
Task.start_link(fn ->
accept_loop(listener)
end)
{:ok, %{listener: listener, acceptor: acceptor, base_url: "http://127.0.0.1:#{port}"}}
end
defp stop_server(%{listener: listener, acceptor: acceptor}) do
:ok = :gen_tcp.close(listener)
if Process.alive?(acceptor) do
Process.exit(acceptor, :normal)
end
end
defp accept_loop(listener) do
case :gen_tcp.accept(listener) do
{:ok, socket} ->
serve(socket)
accept_loop(listener)
{:error, :closed} ->
:ok
{:error, _reason} ->
:ok
end
end
defp serve(socket) do
with {:ok, data} <- recv_headers(socket),
{:ok, path} <- parse_path(data) do
case path do
"/redirect" ->
send_response(socket, 302, "Found", [{"Location", "/final"}], "")
"/final" ->
send_response(socket, 200, "OK", [], "ok")
_ ->
send_response(socket, 404, "Not Found", [], "not found")
end
else
_ -> :ok
end
:gen_tcp.close(socket)
end
defp recv_headers(socket, acc \\ <<>>) do
case :gen_tcp.recv(socket, 0, 1_000) do
{:ok, data} ->
acc = acc <> data
if :binary.match(acc, "\r\n\r\n") != :nomatch do
{:ok, acc}
else
if byte_size(acc) > 8_192 do
{:error, :too_large}
else
recv_headers(socket, acc)
end
end
{:error, _} = error ->
error
end
end
defp parse_path(data) do
case String.split(data, "\r\n", parts: 2) do
[request_line | _] ->
case String.split(request_line, " ") do
[_method, path, _protocol] -> {:ok, path}
_ -> {:error, :invalid_request}
end
_ ->
{:error, :invalid_request}
end
end
defp send_response(socket, status, reason, headers, body) do
base_headers =
[
{"Content-Length", Integer.to_string(byte_size(body))},
{"Connection", "close"}
] ++ headers
iodata =
[
"HTTP/1.1 ",
Integer.to_string(status),
" ",
reason,
"\r\n",
Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
"\r\n",
body
]
:gen_tcp.send(socket, iodata)
end
end

View file

@ -3,10 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTPTest do
use ExUnit.Case, async: true
use ExUnit.Case, async: false
use Pleroma.Tests.Helpers
import Tesla.Mock
alias Pleroma.HTTP
alias Pleroma.Utils.URIEncoding
setup do
mock(fn
@ -25,6 +28,39 @@ defmodule Pleroma.HTTPTest do
%{method: :post, url: "http://example.com/world"} ->
%Tesla.Env{status: 200, body: "world"}
%{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} ->
%Tesla.Env{status: 200, body: "emoji data"}
%{
method: :get,
url: "https://example.com/media/foo/bar%20!$&'()*+,;=/:%20@a%20%5Bbaz%5D.mp4"
} ->
%Tesla.Env{status: 200, body: "video data"}
%{method: :get, url: "https://example.com/media/unicode%20%F0%9F%99%82%20.gif"} ->
%Tesla.Env{status: 200, body: "unicode data"}
%{
method: :get,
url:
"https://i.guim.co.uk/img/media/1069ef13c447908272c4de94174cec2b6352cb2f/0_91_2000_1201/master/2000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctb3BpbmlvbnMtYWdlLTIwMTkucG5n&enable=upscale&s=cba21427a73512fdc9863c486c03fdd8"
} ->
%Tesla.Env{status: 200, body: "Guardian image quirk"}
%{
method: :get,
url:
"https://i.guim.co.uk/emoji/Pack%201/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar+baz"
} ->
%Tesla.Env{status: 200, body: "Space in query with Guardian quirk"}
%{
method: :get,
url:
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
} ->
%Tesla.Env{status: 200, body: "AWS S3 data"}
end)
:ok
@ -67,4 +103,115 @@ defmodule Pleroma.HTTPTest do
}
end
end
test "URL encoding properly encodes URLs with spaces" do
clear_config(:test_url_encoding, true)
url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz"
{:ok, result} = HTTP.get(url_with_space)
assert result.status == 200
properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
{:ok, result} = HTTP.get(properly_encoded_url)
assert result.status == 200
url_with_reserved_chars = "https://example.com/media/foo/bar !$&'()*+,;=/: @a [baz].mp4"
{:ok, result} = HTTP.get(url_with_reserved_chars)
assert result.status == 200
url_with_unicode = "https://example.com/media/unicode 🙂 .gif"
{:ok, result} = HTTP.get(url_with_unicode)
assert result.status == 200
end
test "decodes URL first by default" do
clear_config(:test_url_encoding, true)
normal_url = "https://example.com/media/file%20with%20space.jpg?name=a+space.jpg"
result = URIEncoding.encode_url(normal_url)
assert result == "https://example.com/media/file%20with%20space.jpg?name=a+space.jpg"
end
test "doesn't decode URL first when specified" do
clear_config(:test_url_encoding, true)
normal_url = "https://example.com/media/file%20with%20space.jpg"
result = URIEncoding.encode_url(normal_url, bypass_decode: true)
assert result == "https://example.com/media/file%2520with%2520space.jpg"
end
test "properly applies Guardian image query quirk" do
clear_config(:test_url_encoding, true)
url =
"https://i.guim.co.uk/img/media/1069ef13c447908272c4de94174cec2b6352cb2f/0_91_2000_1201/master/2000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctb3BpbmlvbnMtYWdlLTIwMTkucG5n&enable=upscale&s=cba21427a73512fdc9863c486c03fdd8"
result = URIEncoding.encode_url(url)
assert result == url
{:ok, result_get} = HTTP.get(result)
assert result_get.status == 200
end
test "properly encodes spaces as \"pluses\" in query when using quirks" do
clear_config(:test_url_encoding, true)
url =
"https://i.guim.co.uk/emoji/Pack 1/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar baz"
properly_encoded_url =
"https://i.guim.co.uk/emoji/Pack%201/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar+baz"
result = URIEncoding.encode_url(url)
assert result == properly_encoded_url
{:ok, result_get} = HTTP.get(result)
assert result_get.status == 200
end
test "properly encode AWS S3 queries" do
clear_config(:test_url_encoding, true)
url =
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
unencoded_url =
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY/20130721/us-east-1/s3/aws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
result = URIEncoding.encode_url(url)
result_unencoded = URIEncoding.encode_url(unencoded_url)
assert result == url
assert result == result_unencoded
{:ok, result_get} = HTTP.get(result)
assert result_get.status == 200
end
test "preserves query key order" do
clear_config(:test_url_encoding, true)
url = "https://example.com/foo?hjkl=qwertz&xyz=abc&bar=baz"
result = URIEncoding.encode_url(url)
assert result == url
end
end

View file

@ -3,19 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances.InstanceTest do
alias Pleroma.Instances
alias Pleroma.Instances.Instance
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.CommonAPI
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
import ExUnit.CaptureLog
import Pleroma.Factory
setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
describe "set_reachable/1" do
test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do
unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
@ -31,6 +27,32 @@ defmodule Pleroma.Instances.InstanceTest do
assert {:ok, instance} = Instance.set_reachable(instance.host)
refute instance.unreachable_since
end
test "cancels all ReachabilityWorker jobs for the domain" do
domain = "cancelme.example.org"
insert(:instance, host: domain, unreachable_since: NaiveDateTime.utc_now())
# Insert a ReachabilityWorker job for this domain, scheduled 5 minutes in the future
scheduled_at = DateTime.add(DateTime.utc_now(), 300, :second)
{:ok, job} =
Pleroma.Workers.ReachabilityWorker.new(
%{"domain" => domain, "phase" => "phase_1min", "attempt" => 1},
scheduled_at: scheduled_at
)
|> Oban.insert()
# Ensure the job is present
job = Pleroma.Repo.get(Oban.Job, job.id)
assert job
# Call set_reachable, which should delete the job
assert {:ok, _} = Instance.set_reachable(domain)
# Reload the job and assert it is deleted
job = Pleroma.Repo.get(Oban.Job, job.id)
refute job
end
end
describe "set_unreachable/1" do
@ -145,7 +167,11 @@ defmodule Pleroma.Instances.InstanceTest do
end
test "Doesn't scrapes unreachable instances" do
instance = insert(:instance, unreachable_since: Instances.reachability_datetime_threshold())
instance =
insert(:instance,
unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.add(-:timer.hours(24))
)
url = "https://" <> instance.host
assert capture_log(fn -> assert nil == Instance.get_or_update_favicon(URI.parse(url)) end) =~
@ -213,32 +239,44 @@ defmodule Pleroma.Instances.InstanceTest do
end
end
test "delete_users_and_activities/1 deletes remote instance users and activities" do
[mario, luigi, _peach, wario] =
users = [
insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario"),
insert(:user, nickname: "luigi@mushroom.kingdom", name: "Luigi"),
insert(:user, nickname: "peach@mushroom.kingdom", name: "Peach"),
insert(:user, nickname: "wario@greedville.biz", name: "Wario")
]
test "delete/1 schedules a job to delete the instance and users" do
insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario")
{:ok, post1} = CommonAPI.post(mario, %{status: "letsa go!"})
{:ok, post2} = CommonAPI.post(luigi, %{status: "itsa me... luigi"})
{:ok, post3} = CommonAPI.post(wario, %{status: "WHA-HA-HA!"})
{:ok, _job} = Instance.delete("mushroom.kingdom")
{:ok, job} = Instance.delete_users_and_activities("mushroom.kingdom")
:ok = ObanHelpers.perform(job)
assert_enqueued(
worker: Pleroma.Workers.DeleteWorker,
args: %{"op" => "delete_instance", "host" => "mushroom.kingdom"}
)
end
[mario, luigi, peach, wario] = Repo.reload(users)
describe "check_unreachable/1" do
test "schedules a ReachabilityWorker job for the given domain" do
domain = "test.example.com"
refute mario.is_active
refute luigi.is_active
refute peach.is_active
refute peach.name == "Peach"
# Call check_unreachable
assert {:ok, _job} = Instance.check_unreachable(domain)
assert wario.is_active
assert wario.name == "Wario"
# Verify that a ReachabilityWorker job was scheduled
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
assert length(jobs) == 1
[job] = jobs
assert job.args["domain"] == domain
end
assert [nil, nil, %{}] = Repo.reload([post1, post2, post3])
test "handles multiple calls for the same domain (uniqueness enforced)" do
domain = "duplicate.example.com"
assert {:ok, _job1} = Instance.check_unreachable(domain)
# Second call for the same domain
assert {:ok, %Oban.Job{conflict?: true}} = Instance.check_unreachable(domain)
# Should only have one job due to uniqueness
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
assert length(jobs) == 1
[job] = jobs
assert job.args["domain"] == domain
end
end
end

View file

@ -6,74 +6,42 @@ defmodule Pleroma.InstancesTest do
alias Pleroma.Instances
use Pleroma.DataCase
setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
use Oban.Testing, repo: Pleroma.Repo
describe "reachable?/1" do
test "returns `true` for host / url with unknown reachability status" do
assert Instances.reachable?("unknown.site")
assert Instances.reachable?("http://unknown.site")
end
test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do
host = "consistently-unreachable.name"
Instances.set_consistently_unreachable(host)
refute Instances.reachable?(host)
refute Instances.reachable?("http://#{host}/path")
end
test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do
url = "http://eventually-unreachable.name/path"
Instances.set_unreachable(url)
assert Instances.reachable?(url)
assert Instances.reachable?(URI.parse(url).host)
end
test "raises FunctionClauseError exception on non-binary input" do
assert_raise FunctionClauseError, fn -> Instances.reachable?(nil) end
assert_raise FunctionClauseError, fn -> Instances.reachable?(1) end
end
end
describe "filter_reachable/1" do
setup do
host = "consistently-unreachable.name"
url1 = "http://eventually-unreachable.com/path"
url2 = "http://domain.com/path"
unreachable_host = "consistently-unreachable.name"
reachable_host = "http://domain.com/path"
Instances.set_consistently_unreachable(host)
Instances.set_unreachable(url1)
Instances.set_unreachable(unreachable_host)
result = Instances.filter_reachable([host, url1, url2, nil])
%{result: result, url1: url1, url2: url2}
result = Instances.filter_reachable([unreachable_host, reachable_host, nil])
%{result: result, reachable_host: reachable_host, unreachable_host: unreachable_host}
end
test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list",
%{result: result, url1: url1, url2: url2} do
assert is_map(result)
assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort()
test "returns a list of only reachable elements",
%{result: result, reachable_host: reachable_host} do
assert is_list(result)
assert [reachable_host] == result
end
test "returns a map with `unreachable_since` values for keys",
%{result: result, url1: url1, url2: url2} do
assert is_map(result)
assert %NaiveDateTime{} = result[url1]
assert is_nil(result[url2])
end
test "returns an empty map for empty list or list containing no hosts / url" do
assert %{} == Instances.filter_reachable([])
assert %{} == Instances.filter_reachable([nil])
test "returns an empty list when provided no data" do
assert [] == Instances.filter_reachable([])
assert [] == Instances.filter_reachable([nil])
end
end
describe "set_reachable/1" do
test "sets unreachable url or host reachable" do
host = "domain.com"
Instances.set_consistently_unreachable(host)
Instances.set_unreachable(host)
refute Instances.reachable?(host)
Instances.set_reachable(host)
@ -103,22 +71,68 @@ defmodule Pleroma.InstancesTest do
end
end
describe "set_consistently_unreachable/1" do
test "sets reachable url or host unreachable" do
url = "http://domain.com?q="
assert Instances.reachable?(url)
describe "check_all_unreachable/0" do
test "schedules ReachabilityWorker jobs for all unreachable instances" do
domain1 = "unreachable1.example.com"
domain2 = "unreachable2.example.com"
domain3 = "unreachable3.example.com"
Instances.set_consistently_unreachable(url)
refute Instances.reachable?(url)
Instances.set_unreachable(domain1)
Instances.set_unreachable(domain2)
Instances.set_unreachable(domain3)
Instances.check_all_unreachable()
# Verify that ReachabilityWorker jobs were scheduled for all unreachable domains
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
assert length(jobs) == 3
domains = Enum.map(jobs, & &1.args["domain"])
assert domain1 in domains
assert domain2 in domains
assert domain3 in domains
end
test "keeps unreachable url or host unreachable" do
host = "site.name"
Instances.set_consistently_unreachable(host)
refute Instances.reachable?(host)
test "does not schedule jobs for reachable instances" do
unreachable_domain = "unreachable.example.com"
reachable_domain = "reachable.example.com"
Instances.set_consistently_unreachable(host)
refute Instances.reachable?(host)
Instances.set_unreachable(unreachable_domain)
Instances.set_reachable(reachable_domain)
Instances.check_all_unreachable()
# Verify that only one job was scheduled (for the unreachable domain)
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
assert length(jobs) == 1
[job] = jobs
assert job.args["domain"] == unreachable_domain
end
end
test "delete_all_unreachable/0 schedules DeleteWorker jobs for all unreachable instances" do
domain1 = "unreachable1.example.com"
domain2 = "unreachable2.example.com"
domain3 = "unreachable3.example.com"
Instances.set_unreachable(domain1)
Instances.set_unreachable(domain2)
Instances.set_unreachable(domain3)
Instances.delete_all_unreachable()
# Verify that DeleteWorker jobs were scheduled for all unreachable domains
jobs = all_enqueued(worker: Pleroma.Workers.DeleteWorker)
assert length(jobs) == 3
domains = Enum.map(jobs, & &1.args["host"])
assert domain1 in domains
assert domain2 in domains
assert domain3 in domains
# Verify all jobs are delete_instance operations
Enum.each(jobs, fn job ->
assert job.args["op"] == "delete_instance"
end)
end
end

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,54 @@ 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
} do
assert {:ok, state} =
Pleroma.Web.MastodonAPI.WebsocketHandler.connect(%{
params: %{"stream" => "user", "access_token" => "invalid"},
connect_info: %{
sec_websocket_headers: [
{"sec-websocket-version", "13"},
{"sec-websocket-protocol", token.token}
]
}
})
assert state.user.id == user.id
assert state.oauth_token.id == token.id
assert state.topics != []
end
test "accepts valid token on client-sent event", %{token: token} do
assert {:ok, pid} = start_socket()
@ -363,7 +449,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
test "accepts the 'list' stream", %{token: token, user: user} do
posting_user = insert(:user)
{:ok, list} = Pleroma.List.create("test", user)
{:ok, list} = Pleroma.List.create(%{title: "test"}, user)
Pleroma.List.follow(list, posting_user)
assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}")
@ -430,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
@ -458,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

@ -0,0 +1,56 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Language.LanguageDetectorTest do
use Pleroma.DataCase, async: true
alias Pleroma.Language.LanguageDetector
alias Pleroma.Language.LanguageDetectorMock
alias Pleroma.StaticStubbedConfigMock
import Mox
setup do
# 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 "it detects text language" do
LanguageDetectorMock
|> expect(:detect, fn _text -> "fr" end)
detected_language = LanguageDetector.detect("Je viens d'atterrir en Tchéquie.")
assert detected_language == "fr"
end
test "it returns nil if text is not long enough" do
# No need to set expectations as the word count check happens before the provider is called
detected_language = LanguageDetector.detect("it returns nil")
assert detected_language == nil
end
test "it returns nil if no provider specified" do
# Override the stub to return nil for the provider
StaticStubbedConfigMock
|> expect(:get, fn [Pleroma.Language.LanguageDetector, :provider] -> nil end)
detected_language = LanguageDetector.detect("this should also return nil")
assert detected_language == nil
end
end

View file

@ -0,0 +1,37 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Language.Translation.DeeplTest do
use Pleroma.Web.ConnCase
alias Pleroma.Language.Translation.Deepl
test "it translates text" do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
{:ok, res} =
Deepl.translate(
"USUNĄĆ ŚLEDZIKA!Wklej to na swojego śledzika. Jeżeli uzbieramy 70% użytkowników nk...to usuną śledzika!!!",
"pl",
"en"
)
assert %{
detected_source_language: "PL",
provider: "DeepL"
} = res
end
test "it returns languages list" do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
assert {:ok, [language | _languages]} = Deepl.supported_languages(:target)
assert is_binary(language)
end
end

View file

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Language.Translation.TranslateLocallyTest do
use Pleroma.DataCase
alias Pleroma.Language.Translation.TranslateLocally
@example_models %{
"de" => %{
"en" => "de-en-base"
},
"en" => %{
"de" => "en-de-base",
"pl" => "en-pl-tiny"
},
"cs" => %{
"en" => "cs-en-base"
},
"pl" => %{
"en" => "pl-en-tiny"
}
}
test "it returns languages list" do
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
assert {:ok, languages} = TranslateLocally.supported_languages(:source)
assert ["cs", "de", "en", "pl"] = languages |> Enum.sort()
end
describe "it returns languages matrix" do
test "without intermediary language" do
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
assert {:ok,
%{
"cs" => ["en"],
"de" => ["en"],
"en" => ["de", "pl"],
"pl" => ["en"]
}} = TranslateLocally.languages_matrix()
end
test "with intermediary language" do
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
clear_config([Pleroma.Language.Translation.TranslateLocally, :intermediary_language], "en")
assert {:ok,
%{
"cs" => ["de", "en", "pl"],
"de" => ["en", "pl"],
"en" => ["de", "pl"],
"pl" => ["de", "en"]
}} = TranslateLocally.languages_matrix()
end
end
end

View file

@ -0,0 +1,28 @@
defmodule Pleroma.Language.TranslationTest do
use Pleroma.Web.ConnCase
alias Pleroma.Language.Translation
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
test "it translates text" do
assert {:ok,
%{
content: "txet emos",
detected_source_language: _,
provider: _
}} = Translation.translate("some text", "en", "uk")
end
test "it stores translation result in cache" do
Translation.translate("some text", "en", "uk")
assert {:ok, result} =
Cachex.get(
:translations_cache,
"en/uk/#{:crypto.hash(:sha256, "some text") |> Base.encode64()}"
)
assert result.content == "txet emos"
end
end

View file

@ -10,22 +10,46 @@ defmodule Pleroma.ListTest do
test "creating a list" do
user = insert(:user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
%Pleroma.List{title: title} = Pleroma.List.get(list.id, user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user)
%Pleroma.List{title: title, exclusive: exclusive} = Pleroma.List.get(list.id, user)
assert title == "title"
assert exclusive == false
end
test "validates title" do
user = insert(:user)
assert {:error, changeset} = Pleroma.List.create("", user)
assert {:error, changeset} = Pleroma.List.create(%{title: ""}, user)
assert changeset.errors == [title: {"can't be blank", [validation: :required]}]
end
test "creating a list with an emoji" do
user = insert(:user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title", emoji: "🕓"}, user)
%Pleroma.List{emoji: emoji} = Pleroma.List.get(list.id, user)
assert emoji == "🕓"
end
test "validates emoji" do
user = insert(:user)
assert {:error, changeset} =
Pleroma.List.create(%{title: "title", emoji: "not an emoji"}, user)
assert changeset.errors == [emoji: {"Invalid emoji", []}]
end
test "updating a list's emoji" do
user = insert(:user)
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
{:ok, %{emoji: emoji}} = Pleroma.List.update(list, %{emoji: "🕓"})
assert "🕓" == emoji
end
test "getting a list not belonging to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user)
ret = Pleroma.List.get(list.id, other_user)
assert is_nil(ret)
end
@ -33,7 +57,7 @@ defmodule Pleroma.ListTest do
test "adding an user to a list" do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
{:ok, %{following: following}} = Pleroma.List.follow(list, other_user)
assert [other_user.follower_address] == following
end
@ -41,7 +65,7 @@ defmodule Pleroma.ListTest do
test "removing an user from a list" do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
{:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
{:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user)
assert [] == following
@ -49,14 +73,27 @@ defmodule Pleroma.ListTest do
test "renaming a list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, %{title: title}} = Pleroma.List.rename(list, "new")
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
{:ok, %{title: title}} = Pleroma.List.update(list, %{title: "new"})
assert "new" == title
end
test "updating a list exclusivity" do
user = insert(:user)
{:ok, %{exclusive: exclusive} = list} =
Pleroma.List.create(%{title: "title", exclusive: true}, user)
assert exclusive == true
{:ok, %{exclusive: exclusive} = list} = Pleroma.List.update(list, %{exclusive: false})
assert exclusive == false
{:ok, %{exclusive: exclusive}} = Pleroma.List.update(list, %{exclusive: true})
assert exclusive == true
end
test "deleting a list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
{:ok, list} = Pleroma.List.delete(list)
assert is_nil(Repo.get(Pleroma.List, list.id))
end
@ -65,7 +102,7 @@ defmodule Pleroma.ListTest do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, list} = Pleroma.List.create("title", user)
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
{:ok, list} = Pleroma.List.follow(list, other_user)
{:ok, list} = Pleroma.List.follow(list, third_user)
{:ok, following} = Pleroma.List.get_following(list)
@ -76,9 +113,9 @@ defmodule Pleroma.ListTest do
test "getting all lists by an user" do
user = insert(:user)
other_user = insert(:user)
{:ok, list_one} = Pleroma.List.create("title", user)
{:ok, list_two} = Pleroma.List.create("other title", user)
{:ok, list_three} = Pleroma.List.create("third title", other_user)
{:ok, list_one} = Pleroma.List.create(%{title: "title"}, user)
{:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user)
{:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user)
lists = Pleroma.List.for_user(user, %{})
assert list_one in lists
assert list_two in lists
@ -88,9 +125,9 @@ defmodule Pleroma.ListTest do
test "getting all lists the user is a member of" do
user = insert(:user)
other_user = insert(:user)
{:ok, list_one} = Pleroma.List.create("title", user)
{:ok, list_two} = Pleroma.List.create("other title", user)
{:ok, list_three} = Pleroma.List.create("third title", other_user)
{:ok, list_one} = Pleroma.List.create(%{title: "title"}, user)
{:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user)
{:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user)
{:ok, list_one} = Pleroma.List.follow(list_one, other_user)
{:ok, list_two} = Pleroma.List.follow(list_two, other_user)
{:ok, list_three} = Pleroma.List.follow(list_three, user)
@ -106,8 +143,8 @@ defmodule Pleroma.ListTest do
not_owner = insert(:user)
member_1 = insert(:user)
member_2 = insert(:user)
{:ok, owned_list} = Pleroma.List.create("owned", owner)
{:ok, not_owned_list} = Pleroma.List.create("not owned", not_owner)
{:ok, owned_list} = Pleroma.List.create(%{title: "owned"}, owner)
{:ok, not_owned_list} = Pleroma.List.create(%{title: "not owned"}, not_owner)
{:ok, owned_list} = Pleroma.List.follow(owned_list, member_1)
{:ok, owned_list} = Pleroma.List.follow(owned_list, member_2)
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1)
@ -123,14 +160,14 @@ defmodule Pleroma.ListTest do
test "get by ap_id" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
assert Pleroma.List.get_by_ap_id(list.ap_id) == list
end
test "memberships" do
user = insert(:user)
member = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
{:ok, list} = Pleroma.List.follow(list, member)
assert Pleroma.List.memberships(member) == [list.ap_id]
@ -140,7 +177,7 @@ defmodule Pleroma.ListTest do
user = insert(:user)
member = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
{:ok, list} = Pleroma.List.follow(list, member)
assert Pleroma.List.member?(list, member)

View file

@ -36,11 +36,12 @@ defmodule Pleroma.MarkerTest do
insert(:notification, user: user, activity: insert(:note_activity))
insert(:notification, user: user, activity: insert(:note_activity))
insert(:marker, timeline: "home", user: user)
%Marker{} = refreshed_marker = refresh_record(marker)
assert Marker.get_markers(
user,
["notifications"]
) == [%Marker{refresh_record(marker) | unread_count: 2}]
) == [%{refreshed_marker | unread_count: 2}]
end
end

View file

@ -308,4 +308,37 @@ defmodule Pleroma.ModerationLogTest do
assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}"
end
end
describe "get_log_entry_message/1" do
setup do
moderator = insert(:user, is_moderator: true)
[moderator: moderator]
end
test "handles unknown action types gracefully", %{moderator: moderator} do
log_entry = %ModerationLog{
data: %{
"actor" => %{"nickname" => moderator.nickname},
"action" => "unknown_action",
"some_data" => "test_value"
}
}
assert ModerationLog.get_log_entry_message(log_entry) =~ moderator.nickname
assert ModerationLog.get_log_entry_message(log_entry) =~ "unknown_action"
end
test "handles malformed log entries gracefully" do
log_entry = %ModerationLog{
data: %{
"action" => "force_password_reset"
# Missing "actor" and "subject" fields
}
}
message = ModerationLog.get_log_entry_message(log_entry)
assert is_binary(message)
assert message =~ "force_password_reset"
end
end
end

View file

@ -17,9 +17,10 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.Streamer
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
@ -446,8 +447,7 @@ defmodule Pleroma.NotificationTest do
describe "set_read_up_to()" do
test "it sets all notifications as read up to a specified notification ID" do
user = insert(:user)
other_user = insert(:user)
[user, other_user] = insert_pair(:user)
{:ok, _activity} =
CommonAPI.post(user, %{
@ -486,6 +486,37 @@ defmodule Pleroma.NotificationTest do
assert m.last_read_id == to_string(n2.id)
end
@tag needs_streamer: true
test "it sends updated marker to the 'user' and the 'user:notification' stream" do
%{user: user, token: oauth_token} = oauth_access(["read"])
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(other_user, %{
status: "hi @#{user.nickname}!"
})
[%{id: notification_id}] = Notification.for_user(user)
notification_id = to_string(notification_id)
task =
Task.async(fn ->
{:ok, _topic} =
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
assert_receive {:text, event}, 4_000
assert %{"event" => "marker", "payload" => payload} = Jason.decode!(event)
assert %{"notifications" => %{"last_read_id" => ^notification_id}} =
Jason.decode!(payload)
end)
Notification.set_read_up_to(user, notification_id)
Task.await(task)
end
end
describe "for_user_since/2" do

View file

@ -27,6 +27,11 @@ defmodule Pleroma.Object.ContainmentTest do
})
end
test "rejects an empty actor list without crashing" do
assert :error ==
Containment.contain_origin("https://example.com/objects/1", %{"actor" => []})
end
test "contain_origin_from_id() catches obvious spoofing attempts" do
data = %{
"id" => "http://example.com/~alyssa/activities/1234.json"

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Object.FetcherTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Web.ActivityPub.ObjectValidator
@ -166,15 +165,89 @@ defmodule Pleroma.Object.FetcherTest do
)
end
test "it resets instance reachability on successful fetch" do
id = "http://mastodon.example.org/@admin/99541947525187367"
Instances.set_consistently_unreachable(id)
refute Instances.reachable?(id)
test "it does not fetch from local instance" do
local_url = Pleroma.Web.Endpoint.url() <> "/objects/local_resource"
{:ok, _object} =
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert {:fetch, {:error, "Trying to fetch local resource"}} =
Fetcher.fetch_object_from_id(local_url)
end
assert Instances.reachable?(id)
test "it validates content-type headers according to ActivityPub spec" do
# Setup a mock for an object with invalid content-type
mock(fn
%{method: :get, url: "https://example.com/objects/invalid-content-type"} ->
%Tesla.Env{
status: 200,
# Not a valid AP content-type
headers: [{"content-type", "application/json"}],
body:
Jason.encode!(%{
"id" => "https://example.com/objects/invalid-content-type",
"type" => "Note",
"content" => "This has an invalid content type",
"actor" => "https://example.com/users/actor",
"attributedTo" => "https://example.com/users/actor"
})
}
end)
assert {:fetch, {:error, {:content_type, "application/json"}}} =
Fetcher.fetch_object_from_id("https://example.com/objects/invalid-content-type")
end
test "it accepts objects with application/ld+json and ActivityStreams profile" do
# Setup a mock for an object with ld+json content-type and AS profile
mock(fn
%{method: :get, url: "https://example.com/objects/valid-ld-json"} ->
%Tesla.Env{
status: 200,
headers: [
{"content-type",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}
],
body:
Jason.encode!(%{
"id" => "https://example.com/objects/valid-ld-json",
"type" => "Note",
"content" => "This has a valid ld+json content type",
"actor" => "https://example.com/users/actor",
"attributedTo" => "https://example.com/users/actor"
})
}
end)
# This should pass if content-type validation works correctly
assert {:ok, object} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://example.com/objects/valid-ld-json"
)
assert object["content"] == "This has a valid ld+json content type"
end
test "it rejects objects with no content-type header" do
# Setup a mock for an object with no content-type header
mock(fn
%{method: :get, url: "https://example.com/objects/no-content-type"} ->
%Tesla.Env{
status: 200,
# No content-type header
headers: [],
body:
Jason.encode!(%{
"id" => "https://example.com/objects/no-content-type",
"type" => "Note",
"content" => "This has no content type header",
"actor" => "https://example.com/users/actor",
"attributedTo" => "https://example.com/users/actor"
})
}
end)
# We want to test that the request fails with a missing content-type error
# but the actual error is {:fetch, {:error, nil}} - we'll check for this format
result = Fetcher.fetch_object_from_id("https://example.com/objects/no-content-type")
assert {:fetch, {:error, nil}} = result
end
end
@ -534,6 +607,110 @@ defmodule Pleroma.Object.FetcherTest do
end
end
describe "cross-domain redirect handling" do
setup do
mock(fn
# Cross-domain redirect with original domain in id
%{method: :get, url: "https://original.test/objects/123"} ->
%Tesla.Env{
status: 200,
url: "https://media.test/objects/123",
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => "https://original.test/objects/123",
"type" => "Note",
"content" => "This is redirected content",
"actor" => "https://original.test/users/actor",
"attributedTo" => "https://original.test/users/actor"
})
}
# Cross-domain redirect with final domain in id
%{method: :get, url: "https://original.test/objects/final-domain-id"} ->
%Tesla.Env{
status: 200,
url: "https://media.test/objects/final-domain-id",
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => "https://media.test/objects/final-domain-id",
"type" => "Note",
"content" => "This has final domain in id",
"actor" => "https://original.test/users/actor",
"attributedTo" => "https://original.test/users/actor"
})
}
# No redirect - same domain
%{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
%Tesla.Env{
status: 200,
url: "https://original.test/objects/different-path",
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => "https://original.test/objects/same-domain-redirect",
"type" => "Note",
"content" => "This has a same-domain redirect",
"actor" => "https://original.test/users/actor",
"attributedTo" => "https://original.test/users/actor"
})
}
# Test case with missing url field in response (common in tests)
%{method: :get, url: "https://original.test/objects/missing-url"} ->
%Tesla.Env{
status: 200,
# No url field
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => "https://original.test/objects/missing-url",
"type" => "Note",
"content" => "This has no URL field in response",
"actor" => "https://original.test/users/actor",
"attributedTo" => "https://original.test/users/actor"
})
}
end)
:ok
end
test "it rejects objects from cross-domain redirects with original domain in id" do
assert {:error, {:cross_domain_redirect, true}} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://original.test/objects/123"
)
end
test "it rejects objects from cross-domain redirects with final domain in id" do
assert {:error, {:cross_domain_redirect, true}} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://original.test/objects/final-domain-id"
)
end
test "it accepts objects with same-domain redirects" do
assert {:ok, data} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://original.test/objects/same-domain-redirect"
)
assert data["content"] == "This has a same-domain redirect"
end
test "it handles responses without URL field (common in tests)" do
assert {:ok, data} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://original.test/objects/missing-url"
)
assert data["content"] == "This has no URL field in response"
end
end
describe "fetch with history" do
setup do
object2 = %{

View file

@ -156,7 +156,7 @@ defmodule Pleroma.ObjectTest do
uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
File.mkdir_p!(uploads_dir)
Pleroma.Backports.mkdir_p!(uploads_dir)
file = %Plug.Upload{
content_type: "image/jpeg",

View file

@ -1,42 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.OTPVersionTest do
use ExUnit.Case, async: true
alias Pleroma.OTPVersion
describe "check/1" do
test "22.4" do
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) ==
"22.4"
end
test "22.1" do
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) ==
"22.1"
end
test "21.1" do
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) ==
"21.1"
end
test "23.0" do
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) ==
"23.0"
end
test "with nonexistent file" do
assert OTPVersion.get_version_from_files([
"test/fixtures/warnings/otp_version/non-exising",
"test/fixtures/warnings/otp_version/22.4"
]) == "22.4"
end
test "empty paths" do
assert OTPVersion.get_version_from_files([]) == nil
end
end
end

View file

@ -0,0 +1,293 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.PleromaCtlTest do
use ExUnit.Case, async: false
@pleroma_ctl Path.expand("../../rel/files/bin/pleroma_ctl", __DIR__)
setup do
tmp_dir =
Path.join(System.tmp_dir!(), "pleroma_ctl_test_#{System.unique_integer([:positive])}")
release_root = Path.join(tmp_dir, "release")
bin_dir = Path.join(release_root, "bin")
File.mkdir_p!(bin_dir)
File.cp!(@pleroma_ctl, Path.join(bin_dir, "pleroma_ctl"))
File.chmod!(Path.join(bin_dir, "pleroma_ctl"), 0o755)
on_exit(fn -> File.rm_rf!(tmp_dir) end)
{:ok, tmp_dir: tmp_dir, release_root: release_root, bin_dir: bin_dir}
end
test "update downloads branch-scoped latest OTP package", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir,
release_root: release_root
} do
stubs_dir = Path.join(tmp_dir, "stubs")
curl_args_path = Path.join(tmp_dir, "curl.args")
File.mkdir_p!(stubs_dir)
write_update_stubs(stubs_dir, curl_args_path, "unused", "glibc")
update_tmp_dir = Path.join(tmp_dir, "update_tmp")
File.mkdir_p!(update_tmp_dir)
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
[
"update",
"--branch",
"develop",
"--flavour",
"amd64",
"--tmp-dir",
update_tmp_dir,
"--no-rm"
],
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
stderr_to_stdout: true
)
assert status == 0, output
assert File.exists?(Path.join(release_root, "bin/marker"))
assert ["-fL", url, "-o", artifact_path] =
curl_args_path
|> File.read!()
|> String.split("\n", trim: true)
assert url ==
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-amd64/latest/pleroma.zip"
assert artifact_path == Path.join(update_tmp_dir, "pleroma.zip")
end
test "update detects stable branch and local flavour", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir,
release_root: release_root
} do
stubs_dir = Path.join(tmp_dir, "stubs")
curl_args_path = Path.join(tmp_dir, "curl.args")
File.mkdir_p!(stubs_dir)
write_update_stubs(stubs_dir, curl_args_path, "x86_64", "glibc")
write_start_erl_data(release_root, "2.10.0")
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
stderr_to_stdout: true
)
assert status == 0, output
assert curl_url(curl_args_path) ==
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-amd64/latest/pleroma.zip"
end
test "update detects develop branch and musl arm flavour", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir,
release_root: release_root
} do
stubs_dir = Path.join(tmp_dir, "stubs")
curl_args_path = Path.join(tmp_dir, "curl.args")
File.mkdir_p!(stubs_dir)
write_update_stubs(stubs_dir, curl_args_path, "armv7l", "musl")
write_start_erl_data(release_root, "2.10.0.develop")
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
stderr_to_stdout: true
)
assert status == 0, output
assert curl_url(curl_args_path) ==
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-arm-musl/latest/pleroma.zip"
end
test "update with zip URL bypasses branch and flavour detection", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir,
release_root: release_root
} do
stubs_dir = Path.join(tmp_dir, "stubs")
curl_args_path = Path.join(tmp_dir, "curl.args")
File.mkdir_p!(stubs_dir)
write_update_stubs(stubs_dir, curl_args_path, "unsupported-arch", "unsupported-libc")
write_start_erl_data(release_root, "2.10.0.custombranch")
custom_url = "https://example.test/custom.zip"
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
[
"update",
"--zip-url",
custom_url,
"--tmp-dir",
create_update_tmp_dir(tmp_dir),
"--no-rm"
],
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
stderr_to_stdout: true
)
assert status == 0, output
assert curl_url(curl_args_path) == custom_url
end
test "passes arguments with spaces and Elixir string metacharacters", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir
} do
capture_path = Path.join(tmp_dir, "captured_args")
eval_path = Path.join(tmp_dir, "pleroma_ctl_eval.exs")
write_executable(Path.join(bin_dir, "pleroma"), """
#!/bin/sh
{
printf '%s\n' 'defmodule Pleroma.ReleaseTasks do'
printf '%s\n' ' def run(args), do: File.write!(System.fetch_env!("PLEROMA_CTL_CAPTURE"), :erlang.term_to_binary(args))'
printf '%s\n' 'end'
printf '%s\n' "$2"
} > "$PLEROMA_CTL_EVAL_FILE"
exec elixir "$PLEROMA_CTL_EVAL_FILE"
""")
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
[
"user",
"",
"has space",
~s(has "quote"),
~s(has \\ backslash),
~S(#{:not_interpolated})
],
env: [
{"PLEROMA_CTL_CAPTURE", capture_path},
{"PLEROMA_CTL_EVAL_FILE", eval_path}
],
stderr_to_stdout: true
)
assert status == 0, output
assert capture_path
|> File.read!()
|> :erlang.binary_to_term() == [
"user",
"",
"has space",
~s(has "quote"),
~s(has \\ backslash),
~S(#{:not_interpolated})
]
end
defp write_executable(path, contents) do
File.write!(path, contents)
File.chmod!(path, 0o755)
end
defp write_start_erl_data(release_root, version) do
releases_dir = Path.join(release_root, "releases")
File.mkdir_p!(releases_dir)
File.write!(Path.join(releases_dir, "start_erl.data"), "erts-15.0 #{version}\n")
end
defp create_update_tmp_dir(tmp_dir) do
update_tmp_dir = Path.join(tmp_dir, "update_tmp")
File.mkdir_p!(update_tmp_dir)
update_tmp_dir
end
defp write_update_stubs(stubs_dir, curl_args_path, arch, libc) do
write_executable(Path.join(stubs_dir, "curl"), """
#!/bin/sh
printf '%s\n' "$@" > "#{curl_args_path}"
while [ $# -gt 0 ]; do
case "$1" in
-o)
artifact="$2"
shift 2
;;
*)
shift
;;
esac
done
: > "$artifact"
""")
write_executable(Path.join(stubs_dir, "unzip"), """
#!/bin/sh
while [ $# -gt 0 ]; do
case "$1" in
-d)
dest="$2"
shift 2
;;
*)
shift
;;
esac
done
mkdir -p "$dest/release/bin"
printf 'marker' > "$dest/release/bin/marker"
""")
write_executable(Path.join(stubs_dir, "uname"), """
#!/bin/sh
printf '%s\n' '#{arch}'
""")
write_executable(Path.join(stubs_dir, "getconf"), getconf_stub(libc))
write_executable(Path.join(stubs_dir, "ldd"), """
#!/bin/sh
printf '%s\n' 'musl libc (mock)'
""")
end
defp getconf_stub("glibc") do
"""
#!/bin/sh
printf '%s\n' 'glibc 2.40'
"""
end
defp getconf_stub(_libc) do
"""
#!/bin/sh
exit 1
"""
end
defp curl_url(curl_args_path) do
["-fL", url, "-o", _artifact_path] =
curl_args_path
|> File.read!()
|> String.split("\n", trim: true)
url
end
end

View file

@ -3,12 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do
use Pleroma.DataCase
use Pleroma.DataCase, async: false
import Pleroma.Factory
import Pleroma.Tests.Helpers
alias Pleroma.ConfigDB
setup do: clear_config(Pleroma.Formatter)
setup_all do: require_migration("20200716195806_autolinker_to_linkify")
test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} do

View file

@ -24,7 +24,8 @@ defmodule Pleroma.RepoTest do
describe "get_assoc/2" do
test "get assoc from preloaded data" do
user = %User{name: "Agent Smith"}
token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user}
%Pleroma.Web.OAuth.Token{} = token = insert(:oauth_token)
token = %{token | user: user}
assert Repo.get_assoc(token, :user) == {:ok, user}
end

View file

@ -0,0 +1,22 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client.TeslaTest do
use ExUnit.Case
import Mox
alias Pleroma.ReverseProxy.Client.Tesla
setup :verify_on_exit!
test "cancels an unfinished stream before releasing its connection" do
conn = self()
stream = [make_ref(), make_ref()]
expect(Pleroma.GunMock, :cancel, fn ^conn, ^stream -> :ok end)
assert :ok = Tesla.close(%{pid: conn, stream: stream})
end
end

View file

@ -63,7 +63,11 @@ defmodule Pleroma.ReverseProxyTest do
|> Plug.Conn.put_req_header("user-agent", "fake/1.0")
|> ReverseProxy.call("/user-agent")
assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()}
# Convert the response to a map without relying on json_response
body = conn.resp_body
assert conn.status == 200
response = Jason.decode!(body)
assert response == %{"user-agent" => Pleroma.Application.user_agent()}
end
test "closed connection", %{conn: conn} do
@ -111,6 +115,7 @@ defmodule Pleroma.ReverseProxyTest do
describe "max_body" do
test "length returns error if content-length more than option", %{conn: conn} do
request_mock(0)
expect(ClientMock, :close, fn _ -> :ok end)
assert capture_log(fn ->
ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
@ -124,25 +129,45 @@ defmodule Pleroma.ReverseProxyTest do
end) == ""
end
test "closes streamed responses with invalid status", %{conn: conn} do
ClientMock
|> expect(:request, fn :get, "/invalid-status", _, _, _ ->
{:ok, 404, [], %{url: "/invalid-status"}}
end)
|> expect(:close, fn %{url: "/invalid-status"} -> :ok end)
ReverseProxy.call(conn, "/invalid-status")
end
test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
stream_mock(3, true)
assert capture_log(fn ->
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 29)
end) =~
"Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
end
test "max_body_length accepts a body exactly at the limit", %{conn: conn} do
stream_mock(4)
conn = ReverseProxy.call(conn, "/stream-bytes/30", max_body_length: 30)
assert byte_size(conn.resp_body) == 30
end
end
describe "HEAD requests" do
test "common", %{conn: conn} do
ClientMock
|> expect(:request, fn :head, "/head", _, _, _ ->
{:ok, 200, [{"content-type", "text/html; charset=utf-8"}]}
{:ok, 200, [{"content-type", "image/png"}]}
end)
conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head")
assert html_response(conn, 200) == ""
assert conn.status == 200
assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
assert conn.resp_body == ""
end
end
@ -193,7 +218,10 @@ defmodule Pleroma.ReverseProxyTest do
test "204", %{conn: conn} do
url = "/status/204"
expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
ClientMock
|> expect(:request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
|> expect(:close, fn %{} -> :ok end)
capture_log(fn ->
conn = ReverseProxy.call(conn, url)
@ -215,6 +243,56 @@ defmodule Pleroma.ReverseProxyTest do
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
end
test "sniffs and preserves an image body with a generic content type", %{conn: conn} do
body = File.read!("test/fixtures/image.jpg")
ClientMock
|> expect(:request, fn :get, "/extensionless", headers, _, _ ->
assert {"accept-encoding", "identity"} in headers
{:ok, 200, [{"content-type", "application/octet-stream"}], %{body: body}}
end)
|> expect(:stream_body, fn %{body: ^body} = client ->
{:ok, body, Map.delete(client, :body)}
end)
|> expect(:stream_body, fn %{} -> :done end)
conn =
ReverseProxy.call(conn, "/extensionless",
sniff_content_type: true,
max_read_duration: :infinity,
req_headers: [{"accept-encoding", "gzip"}]
)
assert conn.resp_body == body
assert Conn.get_resp_header(conn, "content-type") == ["image/jpeg"]
assert Conn.get_resp_header(conn, "content-disposition") == [
"inline; filename=\"inline.jpg\""
]
end
test "preserves a generic content type for a non-image body", %{conn: conn} do
body = "not an image"
ClientMock
|> expect(:request, fn :get, "/extensionless", _, _, _ ->
{:ok, 200, [], %{body: body}}
end)
|> expect(:stream_body, fn %{body: ^body} = client ->
{:ok, body, Map.delete(client, :body)}
end)
|> expect(:stream_body, fn %{} -> :done end)
conn = ReverseProxy.call(conn, "/extensionless", sniff_content_type: true)
assert conn.resp_body == body
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
assert Conn.get_resp_header(conn, "content-disposition") == [
"attachment; filename=\"extensionless\""
]
end
defp headers_mock(_) do
ClientMock
|> expect(:request, fn :get, "/headers", headers, _, _ ->
@ -249,7 +327,10 @@ defmodule Pleroma.ReverseProxyTest do
)
|> ReverseProxy.call("/headers")
%{"headers" => headers} = json_response(conn, 200)
body = conn.resp_body
assert conn.status == 200
response = Jason.decode!(body)
headers = response["headers"]
assert headers["Accept"] == "text/html"
end
@ -262,7 +343,10 @@ defmodule Pleroma.ReverseProxyTest do
)
|> ReverseProxy.call("/headers")
%{"headers" => headers} = json_response(conn, 200)
body = conn.resp_body
assert conn.status == 200
response = Jason.decode!(body)
headers = response["headers"]
refute headers["Accept-Language"]
end
end
@ -281,7 +365,7 @@ defmodule Pleroma.ReverseProxyTest do
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/cache")
assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers
assert {"cache-control", "public, max-age=1209600, immutable"} in conn.resp_headers
end
end
@ -315,6 +399,24 @@ defmodule Pleroma.ReverseProxyTest do
conn = ReverseProxy.call(conn, "/disposition")
assert {"content-type", "image/gif"} in conn.resp_headers
assert {"content-disposition", "inline; filename=\"inline.gif\""} in conn.resp_headers
end
test "forces inline for inline content types overriding upstream attachment", %{
conn: conn
} do
disposition_headers_mock([
{"content-type", "image/png"},
{"content-disposition", "attachment; filename=\"filename.png\""},
{"content-disposition", "attachment; filename=\"duplicate.png\""},
{"content-length", "0"}
])
conn = ReverseProxy.call(conn, "/disposition")
[disposition] = Conn.get_resp_header(conn, "content-disposition")
assert String.starts_with?(disposition, "inline")
refute String.starts_with?(disposition, "attachment")
end
test "with content-disposition header", %{conn: conn} do
@ -327,5 +429,166 @@ defmodule Pleroma.ReverseProxyTest do
assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
end
test "with inline_content_types: true leaves upstream headers untouched", %{
conn: conn
} do
# opt == true: the proxy must not synthesise or rewrite content-disposition.
disposition_headers_mock([
{"content-type", "image/png"},
{"content-disposition", "attachment; filename=\"upstream.png\""},
{"content-length", "0"}
])
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: true)
assert {"content-disposition", "attachment; filename=\"upstream.png\""} in conn.resp_headers
end
test "forces bare inline for a whitelisted type with no MIME extension", %{
conn: conn
} do
# image/x-foo-bar has no entry in MIME's database, so inline_filename/1
# returns nil and the disposition should be the bare token "inline".
disposition_headers_mock([
{"content-type", "image/x-foo-bar"},
{"content-length", "0"}
])
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: ["image/x-foo-bar"])
[disposition] = Conn.get_resp_header(conn, "content-disposition")
assert disposition == "inline"
end
test "serves modern browser image types inline", %{conn: conn} do
disposition_headers_mock([
{"content-type", "image/webp"},
{"content-length", "0"}
])
conn = ReverseProxy.call(conn, "/disposition")
assert Conn.get_resp_header(conn, "content-disposition") == [
"inline; filename=\"inline.webp\""
]
end
end
describe "content-type sanitisation" do
test "preserves allowed image type", %{conn: conn} do
ClientMock
|> expect(:request, fn :get, "/content", _, _, _ ->
{:ok, 200, [{"content-type", "image/png"}], %{url: "/content"}}
end)
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/content")
assert conn.status == 200
assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
end
test "preserves allowed video type", %{conn: conn} do
ClientMock
|> expect(:request, fn :get, "/content", _, _, _ ->
{:ok, 200, [{"content-type", "video/mp4"}], %{url: "/content"}}
end)
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/content")
assert conn.status == 200
assert Conn.get_resp_header(conn, "content-type") == ["video/mp4"]
end
test "sanitizes ActivityPub content type", %{conn: conn} do
ClientMock
|> expect(:request, fn :get, "/content", _, _, _ ->
{:ok, 200, [{"content-type", "application/activity+json"}], %{url: "/content"}}
end)
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/content")
assert conn.status == 200
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
end
test "sanitizes LD-JSON content type", %{conn: conn} do
ClientMock
|> expect(:request, fn :get, "/content", _, _, _ ->
{:ok, 200, [{"content-type", "application/ld+json"}], %{url: "/content"}}
end)
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/content")
assert conn.status == 200
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
end
end
# Hackney is used for Reverse Proxy when Hackney or Finch is the Tesla Adapter
# Gun is able to proxy through Tesla, so it does not need testing as the
# test cases in the Pleroma.HTTPTest module are sufficient
describe "Hackney URL encoding:" do
setup do
ClientMock
|> expect(:request, fn
:get,
"https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz",
_headers,
_body,
_opts ->
{:ok, 200, [{"content-type", "image/png"}], "It works!"}
:get,
"https://example.com/media/foo/bar%20!$&'()*+,;=/:%20@a%20%5Bbaz%5D.mp4",
_headers,
_body,
_opts ->
{:ok, 200, [{"content-type", "video/mp4"}], "Allowed reserved chars."}
:get, "https://example.com/media/unicode%20%F0%9F%99%82%20.gif", _headers, _body, _opts ->
{:ok, 200, [{"content-type", "image/gif"}], "Unicode emoji in path"}
end)
|> stub(:stream_body, fn _ -> :done end)
|> stub(:close, fn _ -> :ok end)
:ok
end
test "properly encodes URLs with spaces", %{conn: conn} do
url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz"
result = ReverseProxy.call(conn, url_with_space)
assert result.status == 200
end
test "properly encoded URL should not be altered", %{conn: conn} do
properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
result = ReverseProxy.call(conn, properly_encoded_url)
assert result.status == 200
end
test "properly encodes URLs with allowed reserved characters", %{conn: conn} do
url_with_reserved_chars = "https://example.com/media/foo/bar !$&'()*+,;=/: @a [baz].mp4"
result = ReverseProxy.call(conn, url_with_reserved_chars)
assert result.status == 200
end
test "properly encodes URLs with unicode in path", %{conn: conn} do
url_with_unicode = "https://example.com/media/unicode 🙂 .gif"
result = ReverseProxy.call(conn, url_with_unicode)
assert result.status == 200
end
end
end

View file

@ -0,0 +1,496 @@
defmodule Pleroma.SafeZipTest do
# Not making this async because it creates and deletes files
use ExUnit.Case
alias Pleroma.SafeZip
@fixtures_dir "test/fixtures"
@tmp_dir "test/zip_tmp"
setup do
# Ensure tmp directory exists
Pleroma.Backports.mkdir_p!(@tmp_dir)
on_exit(fn ->
# Clean up any files created during tests
File.rm_rf!(@tmp_dir)
Pleroma.Backports.mkdir_p!(@tmp_dir)
end)
:ok
end
describe "list_dir_file/1" do
test "lists files in a valid zip" do
{:ok, files} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "emojis.zip"))
assert is_list(files)
assert length(files) > 0
end
test "returns an empty list for empty zip" do
{:ok, files} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "empty.zip"))
assert files == []
end
test "returns error for non-existent file" do
assert {:error, _} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "nonexistent.zip"))
end
test "only lists regular files, not directories" do
# Create a zip with both files and directories
zip_path = create_zip_with_directory()
# List files with SafeZip
{:ok, files} = SafeZip.list_dir_file(zip_path)
# Verify only regular files are listed, not directories
assert "file_in_dir/test_file.txt" in files
assert "root_file.txt" in files
# Directory entries should not be included in the list
refute "file_in_dir/" in files
end
end
describe "contains_all_data?/2" do
test "returns true when all files are in the archive" do
# For this test, we'll create our own zip file with known content
# to ensure we can test the contains_all_data? function properly
zip_path = create_zip_with_directory()
archive_data = File.read!(zip_path)
# Check if the archive contains the root file
# Note: The function expects charlists (Erlang strings) in the MapSet
assert SafeZip.contains_all_data?(archive_data, MapSet.new([~c"root_file.txt"]))
end
test "returns false when files are missing" do
archive_path = Path.join(@fixtures_dir, "emojis.zip")
archive_data = File.read!(archive_path)
# Create a MapSet with non-existent files
fset = MapSet.new([~c"nonexistent.txt"])
refute SafeZip.contains_all_data?(archive_data, fset)
end
test "returns false for invalid archive data" do
refute SafeZip.contains_all_data?("invalid data", MapSet.new([~c"file.txt"]))
end
test "only checks for regular files, not directories" do
# Create a zip with both files and directories
zip_path = create_zip_with_directory()
archive_data = File.read!(zip_path)
# Check if the archive contains a directory (should return false)
refute SafeZip.contains_all_data?(archive_data, MapSet.new([~c"file_in_dir/"]))
# For this test, we'll manually check if the file exists in the archive
# by extracting it and verifying it exists
extract_dir = Path.join(@tmp_dir, "extract_check")
Pleroma.Backports.mkdir_p!(extract_dir)
{:ok, files} = SafeZip.unzip_file(zip_path, extract_dir)
# Verify the root file was extracted
assert Enum.any?(files, fn file ->
Path.basename(file) == "root_file.txt"
end)
# Verify the file exists on disk
assert File.exists?(Path.join(extract_dir, "root_file.txt"))
end
end
describe "zip/4" do
test "creates a zip file on disk" do
# Create a test file
test_file_path = Path.join(@tmp_dir, "test_file.txt")
File.write!(test_file_path, "test content")
# Create a zip file
zip_path = Path.join(@tmp_dir, "test.zip")
assert {:ok, ^zip_path} = SafeZip.zip(zip_path, ["test_file.txt"], @tmp_dir, false)
# Verify the zip file exists
assert File.exists?(zip_path)
end
test "creates a zip file in memory" do
# Create a test file
test_file_path = Path.join(@tmp_dir, "test_file.txt")
File.write!(test_file_path, "test content")
# Create a zip file in memory
zip_name = Path.join(@tmp_dir, "test.zip")
assert {:ok, {^zip_name, zip_data}} =
SafeZip.zip(zip_name, ["test_file.txt"], @tmp_dir, true)
# Verify the zip data is binary
assert is_binary(zip_data)
end
test "returns error for unsafe paths" do
# Try to zip a file with path traversal
assert {:error, _} =
SafeZip.zip(
Path.join(@tmp_dir, "test.zip"),
["../fixtures/test.txt"],
@tmp_dir,
false
)
end
test "can create zip with directories" do
# Create a directory structure
dir_path = Path.join(@tmp_dir, "test_dir")
Pleroma.Backports.mkdir_p!(dir_path)
file_in_dir_path = Path.join(dir_path, "file_in_dir.txt")
File.write!(file_in_dir_path, "file in directory")
# Create a zip file
zip_path = Path.join(@tmp_dir, "dir_test.zip")
assert {:ok, ^zip_path} =
SafeZip.zip(
zip_path,
["test_dir/file_in_dir.txt"],
@tmp_dir,
false
)
# Verify the zip file exists
assert File.exists?(zip_path)
# Extract and verify the directory structure is preserved
extract_dir = Path.join(@tmp_dir, "extract")
{:ok, files} = SafeZip.unzip_file(zip_path, extract_dir)
# Check if the file path is in the list, accounting for possible full paths
assert Enum.any?(files, fn file ->
String.ends_with?(file, "file_in_dir.txt")
end)
# Verify the file exists in the expected location
assert File.exists?(Path.join([extract_dir, "test_dir", "file_in_dir.txt"]))
end
end
describe "unzip_file/3" do
test "extracts files from a zip archive" do
archive_path = Path.join(@fixtures_dir, "emojis.zip")
# Extract the archive
assert {:ok, files} = SafeZip.unzip_file(archive_path, @tmp_dir)
# Verify files were extracted
assert is_list(files)
assert length(files) > 0
# Verify at least one file exists
first_file = List.first(files)
# Simply check that the file exists in the tmp directory
assert File.exists?(first_file)
end
test "extracts specific files from a zip archive" do
archive_path = Path.join(@fixtures_dir, "emojis.zip")
# Get list of files in the archive
{:ok, all_files} = SafeZip.list_dir_file(archive_path)
file_to_extract = List.first(all_files)
# Extract only one file
assert {:ok, [extracted_file]} =
SafeZip.unzip_file(archive_path, @tmp_dir, [file_to_extract])
# Verify only the specified file was extracted
assert Path.basename(extracted_file) == Path.basename(file_to_extract)
# Check that the file exists in the tmp directory
assert File.exists?(Path.join(@tmp_dir, Path.basename(file_to_extract)))
end
test "returns error for invalid zip file" do
invalid_path = Path.join(@tmp_dir, "invalid.zip")
File.write!(invalid_path, "not a zip file")
assert {:error, _} = SafeZip.unzip_file(invalid_path, @tmp_dir)
end
test "creates directories when extracting files in subdirectories" do
# Create a zip with files in subdirectories
zip_path = create_zip_with_directory()
# Extract the archive
assert {:ok, files} = SafeZip.unzip_file(zip_path, @tmp_dir)
# Verify files were extracted - handle both relative and absolute paths
assert Enum.any?(files, fn file ->
Path.basename(file) == "test_file.txt" &&
String.contains?(file, "file_in_dir")
end)
assert Enum.any?(files, fn file ->
Path.basename(file) == "root_file.txt"
end)
# Verify directory was created
dir_path = Path.join(@tmp_dir, "file_in_dir")
assert File.exists?(dir_path)
assert File.dir?(dir_path)
# Verify file in directory was extracted
file_path = Path.join(dir_path, "test_file.txt")
assert File.exists?(file_path)
end
end
describe "unzip_data/3" do
test "extracts files from zip data" do
archive_path = Path.join(@fixtures_dir, "emojis.zip")
archive_data = File.read!(archive_path)
# Extract the archive from data
assert {:ok, files} = SafeZip.unzip_data(archive_data, @tmp_dir)
# Verify files were extracted
assert is_list(files)
assert length(files) > 0
# Verify at least one file exists
first_file = List.first(files)
# Simply check that the file exists in the tmp directory
assert File.exists?(first_file)
end
test "extracts specific files from zip data" do
archive_path = Path.join(@fixtures_dir, "emojis.zip")
archive_data = File.read!(archive_path)
# Get list of files in the archive
{:ok, all_files} = SafeZip.list_dir_file(archive_path)
file_to_extract = List.first(all_files)
# Extract only one file
assert {:ok, extracted_files} =
SafeZip.unzip_data(archive_data, @tmp_dir, [file_to_extract])
# Verify only the specified file was extracted
assert Enum.any?(extracted_files, fn path ->
Path.basename(path) == Path.basename(file_to_extract)
end)
# Simply check that the file exists in the tmp directory
assert File.exists?(Path.join(@tmp_dir, Path.basename(file_to_extract)))
end
test "returns error for invalid zip data" do
assert {:error, _} = SafeZip.unzip_data("not a zip file", @tmp_dir)
end
test "creates directories when extracting files in subdirectories from data" do
# Create a zip with files in subdirectories
zip_path = create_zip_with_directory()
archive_data = File.read!(zip_path)
# Extract the archive from data
assert {:ok, files} = SafeZip.unzip_data(archive_data, @tmp_dir)
# Verify files were extracted - handle both relative and absolute paths
assert Enum.any?(files, fn file ->
Path.basename(file) == "test_file.txt" &&
String.contains?(file, "file_in_dir")
end)
assert Enum.any?(files, fn file ->
Path.basename(file) == "root_file.txt"
end)
# Verify directory was created
dir_path = Path.join(@tmp_dir, "file_in_dir")
assert File.exists?(dir_path)
assert File.dir?(dir_path)
# Verify file in directory was extracted
file_path = Path.join(dir_path, "test_file.txt")
assert File.exists?(file_path)
end
end
# Security tests
describe "security checks" do
test "prevents path traversal in zip extraction" do
# Create a malicious zip file with path traversal
malicious_zip_path = create_malicious_zip_with_path_traversal()
# Try to extract it with SafeZip
assert {:error, _} = SafeZip.unzip_file(malicious_zip_path, @tmp_dir)
# Verify the file was not extracted outside the target directory
refute File.exists?(Path.join(Path.dirname(@tmp_dir), "traversal_attempt.txt"))
end
test "prevents directory traversal in zip listing" do
# Create a malicious zip file with path traversal
malicious_zip_path = create_malicious_zip_with_path_traversal()
# Try to list files with SafeZip
assert {:error, _} = SafeZip.list_dir_file(malicious_zip_path)
end
test "prevents path traversal in zip data extraction" do
# Create a malicious zip file with path traversal
malicious_zip_path = create_malicious_zip_with_path_traversal()
malicious_data = File.read!(malicious_zip_path)
# Try to extract it with SafeZip
assert {:error, _} = SafeZip.unzip_data(malicious_data, @tmp_dir)
# Verify the file was not extracted outside the target directory
refute File.exists?(Path.join(Path.dirname(@tmp_dir), "traversal_attempt.txt"))
end
test "handles zip bomb attempts" do
# Create a zip bomb (a zip with many files or large files)
zip_bomb_path = create_zip_bomb()
# The SafeZip module should handle this gracefully
# Either by successfully extracting it (if it's not too large)
# or by returning an error (if it detects a potential zip bomb)
result = SafeZip.unzip_file(zip_bomb_path, @tmp_dir)
case result do
{:ok, _} ->
# If it successfully extracts, make sure it didn't fill up the disk
# This is a simple check to ensure the extraction was controlled
assert File.exists?(@tmp_dir)
{:error, _} ->
# If it returns an error, that's also acceptable
# The important thing is that it doesn't crash or hang
assert true
end
end
test "handles deeply nested directory structures" do
# Create a zip with deeply nested directories
deep_nest_path = create_deeply_nested_zip()
# The SafeZip module should handle this gracefully
result = SafeZip.unzip_file(deep_nest_path, @tmp_dir)
case result do
{:ok, files} ->
# If it successfully extracts, verify the files were extracted
assert is_list(files)
assert length(files) > 0
{:error, _} ->
# If it returns an error, that's also acceptable
# The important thing is that it doesn't crash or hang
assert true
end
end
end
# Helper functions to create test fixtures
# Creates a zip file with a path traversal attempt
defp create_malicious_zip_with_path_traversal do
malicious_zip_path = Path.join(@tmp_dir, "path_traversal.zip")
# Create a file to include in the zip
test_file_path = Path.join(@tmp_dir, "test_file.txt")
File.write!(test_file_path, "malicious content")
# Use Erlang's zip module directly to create a zip with path traversal
{:ok, charlist_path} =
:zip.create(
String.to_charlist(malicious_zip_path),
[{String.to_charlist("../traversal_attempt.txt"), File.read!(test_file_path)}]
)
to_string(charlist_path)
end
# Creates a zip file with directory entries
defp create_zip_with_directory do
zip_path = Path.join(@tmp_dir, "with_directory.zip")
# Create files to include in the zip
root_file_path = Path.join(@tmp_dir, "root_file.txt")
File.write!(root_file_path, "root file content")
# Create a directory and a file in it
dir_path = Path.join(@tmp_dir, "file_in_dir")
Pleroma.Backports.mkdir_p!(dir_path)
file_in_dir_path = Path.join(dir_path, "test_file.txt")
File.write!(file_in_dir_path, "file in directory content")
# Use Erlang's zip module to create a zip with directory structure
{:ok, charlist_path} =
:zip.create(
String.to_charlist(zip_path),
[
{String.to_charlist("root_file.txt"), File.read!(root_file_path)},
{String.to_charlist("file_in_dir/test_file.txt"), File.read!(file_in_dir_path)}
]
)
to_string(charlist_path)
end
# Creates a zip bomb (a zip with many small files)
defp create_zip_bomb do
zip_path = Path.join(@tmp_dir, "zip_bomb.zip")
# Create a small file to duplicate many times
small_file_path = Path.join(@tmp_dir, "small_file.txt")
File.write!(small_file_path, String.duplicate("A", 100))
# Create a list of many files to include in the zip
file_entries =
for i <- 1..100 do
{String.to_charlist("file_#{i}.txt"), File.read!(small_file_path)}
end
# Use Erlang's zip module to create a zip with many files
{:ok, charlist_path} =
:zip.create(
String.to_charlist(zip_path),
file_entries
)
to_string(charlist_path)
end
# Creates a zip with deeply nested directories
defp create_deeply_nested_zip do
zip_path = Path.join(@tmp_dir, "deep_nest.zip")
# Create a file to include in the zip
file_content = "test content"
# Create a list of deeply nested files
file_entries =
for i <- 1..10 do
nested_path = Enum.reduce(1..i, "nested", fn j, acc -> "#{acc}/level_#{j}" end)
{String.to_charlist("#{nested_path}/file.txt"), file_content}
end
# Use Erlang's zip module to create a zip with deeply nested directories
{:ok, charlist_path} =
:zip.create(
String.to_charlist(zip_path),
file_entries
)
to_string(charlist_path)
end
end

View file

@ -0,0 +1,79 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.BackfillTest do
use Pleroma.DataCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Search.Backfill
alias Pleroma.Workers.SearchIndexingWorker
test "enqueues the exact requested number across partial pages" do
activities = insert_list(5, :note_activity) |> Enum.sort_by(& &1.id, :desc)
assert %{enqueued: 3, exhausted: false, next_cursor: cursor} =
Backfill.run(limit: 3, step: 2, chunk: 2)
assert search_indexing_ids() == Enum.map(Enum.take(activities, 3), & &1.id)
assert cursor == Enum.at(activities, 2).id
end
test "resumes after the last processed activity without duplicates" do
activities = insert_list(5, :note_activity) |> Enum.sort_by(& &1.id, :desc)
assert %{enqueued: 2, next_cursor: cursor} = Backfill.run(limit: 2, step: 1)
Pleroma.Repo.delete_all(Oban.Job)
assert %{enqueued: 2, next_cursor: next_cursor} =
Backfill.run(before: cursor, limit: 2, step: 1)
assert search_indexing_ids() ==
activities
|> Enum.slice(2, 2)
|> Enum.map(& &1.id)
assert next_cursor == Enum.at(activities, 3).id
end
test "only queues Create activities" do
create = insert(:note_activity)
insert(:like_activity, note_activity: create)
assert %{enqueued: 1, exhausted: true, next_cursor: cursor} = Backfill.run(step: 2)
assert search_indexing_ids() == [create.id]
assert cursor == create.id
end
test "rejects unknown options and malformed cursors before enqueueing" do
assert_raise ArgumentError, fn -> Backfill.run(limt: 1) end
assert_raise ArgumentError, fn -> Backfill.run(before: "not-an-activity-id") end
assert_raise ArgumentError, fn -> Backfill.run(on_page: :invalid) end
assert all_enqueued(worker: SearchIndexingWorker) == []
end
test "does not duplicate or skip the initial activities when newer rows are inserted" do
activities = insert_list(3, :note_activity) |> Enum.sort_by(& &1.id, :desc)
assert %{enqueued: 3, exhausted: true} =
Backfill.run(
limit: :infinity,
step: 1,
on_page: fn %{enqueued: enqueued} ->
if enqueued == 1, do: insert(:note_activity)
end
)
assert search_indexing_ids() == Enum.map(activities, & &1.id)
end
defp search_indexing_ids do
all_enqueued(worker: SearchIndexingWorker)
|> Enum.sort_by(& &1.id)
|> Enum.map(& &1.args["activity"])
end
end

View file

@ -74,29 +74,6 @@ defmodule Pleroma.Search.MeilisearchTest do
assert_received("posted_to_meilisearch")
end
test "doesn't index posts that are not public" do
user = insert(:user)
Enum.each(["private", "direct"], fn visibility ->
{:ok, activity} =
CommonAPI.post(user, %{
status: "guys i just don't wanna leave the swamp",
visibility: visibility
})
args = %{"op" => "add_to_index", "activity" => activity.id}
Config
|> expect(:get, fn
[Pleroma.Search, :module], nil ->
Meilisearch
end)
assert_enqueued(worker: SearchIndexingWorker, args: args)
assert :ok = perform_job(SearchIndexingWorker, args)
end)
end
test "deletes posts from index when deleted locally" do
user = insert(:user)

View file

@ -0,0 +1,323 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.ParadeDBIntegrationTest do
use Pleroma.DataCase, async: false
@moduletag :integration
@moduletag timeout: 180_000
import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.Search.ParadeDB
alias Pleroma.Web.CommonAPI
@image System.get_env(
"PARADEDB_TEST_IMAGE",
"paradedb/paradedb:0.24.3-pg15@sha256:997d62f6c2605becb7c45878af3f8c8c698f355006b675b3ce212d4eb2d14f2e"
)
@db_user "postgres"
@db_password "postgres"
@db_name "paradedb"
@docker_startup_timeout_ms 60_000
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
:ok
end
setup_all do
case System.get_env("PARADEDB_TEST_URL") do
nil -> setup_docker_paradedb()
url -> setup_external_paradedb(url)
end
end
defp setup_docker_paradedb do
with :ok <- ensure_docker_available() do
container = "pleroma-paradedb-it-#{System.unique_integer([:positive])}"
on_exit(fn ->
_ = docker(["rm", "-f", container])
end)
{id, 0} =
docker([
"run",
"--rm",
"--name",
container,
"-e",
"POSTGRES_USER=#{@db_user}",
"-e",
"POSTGRES_PASSWORD=#{@db_password}",
"-e",
"POSTGRES_DB=#{@db_name}",
"-p",
"127.0.0.1::5432",
"-d",
@image
])
container_id = String.trim(id)
host_port =
container
|> wait_for_port!(@docker_startup_timeout_ms)
wait_for_pg_ready!(container, @docker_startup_timeout_ms)
url = "postgres://#{@db_user}:#{@db_password}@127.0.0.1:#{host_port}/#{@db_name}"
setup_repo(url, %{container: container, container_id: container_id})
end
end
defp setup_external_paradedb(url) do
uri = URI.parse(url)
wait_for_tcp_ready!(uri.host, uri.port || 5432, @docker_startup_timeout_ms)
setup_repo(url, %{})
end
defp setup_repo(url, context) do
prev_env = System.get_env("PARADEDB_DATABASE_URL")
System.put_env("PARADEDB_DATABASE_URL", url)
on_exit(fn ->
if is_nil(prev_env) do
System.delete_env("PARADEDB_DATABASE_URL")
else
System.put_env("PARADEDB_DATABASE_URL", prev_env)
end
end)
{:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 5})
wait_for_repo_ready!(@docker_startup_timeout_ms)
previous_table = Pleroma.Config.get([Pleroma.Search.ParadeDB, :table], nil)
suffix = Ecto.UUID.generate() |> String.replace("-", "")
table = "pleroma_search_documents_it_#{suffix}"
Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], table)
on_exit(fn ->
if is_nil(previous_table) do
Pleroma.Config.delete([Pleroma.Search.ParadeDB, :table])
else
Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], previous_table)
end
end)
{:ok, Map.merge(context, %{url: url, table: table})}
end
test "indexes and searches against real ParadeDB", %{table: table} do
on_exit(fn ->
_ =
Ecto.Adapters.SQL.query(
Pleroma.Search.ParadeDB.Repo,
"DROP TABLE IF EXISTS #{table}"
)
end)
:ok = ParadeDB.drop_index()
assert :ok = ParadeDB.create_index()
assert {:ok, %{rows: [["0.24.3"]]}} =
Ecto.Adapters.SQL.query(
Pleroma.Search.ParadeDB.Repo,
"SELECT extversion FROM pg_extension WHERE extname = 'pg_search'"
)
user = insert(:user)
{:ok, a1} =
CommonAPI.post(user, %{status: "paradedb integration first", visibility: "public"})
:timer.sleep(1100)
{:ok, a2} =
CommonAPI.post(user, %{status: "paradedb integration second", visibility: "public"})
{:ok, private} =
CommonAPI.post(user, %{status: "paradedb integration private", visibility: "private"})
a1 = Activity.get_by_id_with_object(a1.id)
a2 = Activity.get_by_id_with_object(a2.id)
private = Activity.get_by_id_with_object(private.id)
a1 = %{
a1
| object: %{
a1.object
| data:
Map.merge(a1.object.data, %{
"summary" => "paradedb subject phrase",
"attachment" => [
%{
"name" => "remote-image.jpg",
"summary" => "paradedb attachment description"
}
]
})
}
}
assert :ok = ParadeDB.add_to_index(a1)
assert :ok = ParadeDB.add_to_index(a2)
assert :ok = ParadeDB.add_to_index(private)
results = ParadeDB.search(nil, "paradedb integration", limit: 40, offset: 0)
ids = Enum.map(results, & &1.id)
assert a1.id in ids
assert a2.id in ids
refute private.id in ids
assert List.first(ids) == a2.id
assert Enum.any?(ParadeDB.search(nil, "subject phrase"), &(&1.id == a1.id))
assert Enum.any?(ParadeDB.search(nil, "attachment description"), &(&1.id == a1.id))
previous_fuzzy_distance = Pleroma.Config.get([Pleroma.Search.ParadeDB, :fuzzy_distance], 0)
Pleroma.Config.put([Pleroma.Search.ParadeDB, :fuzzy_distance], 1)
on_exit(fn ->
Pleroma.Config.put([Pleroma.Search.ParadeDB, :fuzzy_distance], previous_fuzzy_distance)
end)
fuzzy_results = ParadeDB.search(nil, "paradedb integraion", limit: 40, offset: 0)
fuzzy_ids = Enum.map(fuzzy_results, & &1.id)
assert a1.id in fuzzy_ids
assert a2.id in fuzzy_ids
refute private.id in fuzzy_ids
# Removing an object should remove it from the ParadeDB index.
assert :ok = ParadeDB.remove_from_index(a2.object)
results_after_delete = ParadeDB.search(nil, "paradedb integration", limit: 40, offset: 0)
ids_after_delete = Enum.map(results_after_delete, & &1.id)
assert a1.id in ids_after_delete
refute a2.id in ids_after_delete
end
defp ensure_docker_available do
if is_nil(System.find_executable("docker")) do
{:skip, "Docker not installed"}
else
case docker(["info"]) do
{_out, 0} -> :ok
{out, _} -> {:skip, "Docker not available: #{String.trim(out)}"}
end
end
end
defp wait_for_port!(container, timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
wait = fn wait ->
case docker(["port", container, "5432/tcp"]) do
{out, 0} when byte_size(out) > 0 ->
out
|> String.split("\n", trim: true)
|> List.first()
|> parse_host_port!()
_ ->
if System.monotonic_time(:millisecond) > deadline do
raise "Timed out waiting for docker port mapping"
else
:timer.sleep(250)
wait.(wait)
end
end
end
wait.(wait)
end
defp parse_host_port!(mapping) do
mapping
|> String.trim()
|> String.split(":")
|> List.last()
|> String.to_integer()
end
defp wait_for_pg_ready!(container, timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
wait = fn wait ->
case docker([
"exec",
container,
"pg_isready",
"-U",
@db_user,
"-d",
@db_name
]) do
{_out, 0} ->
:ok
_ ->
if System.monotonic_time(:millisecond) > deadline do
raise "Timed out waiting for ParadeDB Postgres to become ready"
else
:timer.sleep(250)
wait.(wait)
end
end
end
wait.(wait)
end
defp wait_for_tcp_ready!(host, port, timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
wait = fn wait ->
case :gen_tcp.connect(String.to_charlist(host), port, [:binary, active: false], 500) do
{:ok, socket} ->
:gen_tcp.close(socket)
:ok
{:error, _} ->
if System.monotonic_time(:millisecond) > deadline do
raise "Timed out waiting for TCP port #{port} to accept connections"
else
:timer.sleep(250)
wait.(wait)
end
end
end
wait.(wait)
end
defp wait_for_repo_ready!(timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
wait = fn wait ->
case Ecto.Adapters.SQL.query(Pleroma.Search.ParadeDB.Repo, "SELECT 1") do
{:ok, _} ->
:ok
_ ->
if System.monotonic_time(:millisecond) > deadline do
raise "Timed out waiting for ParadeDB Repo"
else
:timer.sleep(250)
wait.(wait)
end
end
end
wait.(wait)
end
defp docker(args) do
System.cmd("docker", args, stderr_to_stdout: true)
end
end

View file

@ -0,0 +1,286 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.ParadeDBTest do
use Pleroma.DataCase, async: true
use Oban.Testing, repo: Pleroma.Repo
import Mox
import Pleroma.Factory
alias Pleroma.Search.ParadeDB
alias Pleroma.Search.ParadeDB.ClientMock
alias Pleroma.UnstubbedConfigMock, as: Config
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.SearchIndexingWorker
describe "ParadeDB" do
test "indexes a public post on creation" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "guys i just don't wanna leave the swamp",
visibility: "public"
})
args = %{"op" => "add_to_index", "activity" => activity.id}
assert_enqueued(worker: SearchIndexingWorker, args: args)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "INSERT INTO pleroma_search_documents"
[activity_id, object_id, object_ap_id, actor_ap_id, content, published_at] = params
{:ok, dumped_activity_id} = FlakeId.Ecto.CompatType.dump(activity.id)
assert activity_id == dumped_activity_id
assert is_integer(object_id)
assert is_binary(object_ap_id)
assert actor_ap_id == activity.data["actor"]
assert content == "guys i just don&#39;t wanna leave the swamp"
assert %DateTime{} = published_at
send(self(), :inserted)
{:ok, %{}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search, :module], nil ->
ParadeDB
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
end)
assert :ok = perform_job(SearchIndexingWorker, args)
assert_received(:inserted)
end
test "doesn't index posts that are not public" do
user = insert(:user)
Enum.each(["private", "direct"], fn visibility ->
{:ok, activity} =
CommonAPI.post(user, %{
status: "guys i just don't wanna leave the swamp",
visibility: visibility
})
args = %{"op" => "add_to_index", "activity" => activity.id}
refute_enqueued(worker: SearchIndexingWorker, args: args)
end)
end
test "backend skips non-public posts when called directly" do
user = insert(:user)
Enum.each(["private", "direct"], fn visibility ->
{:ok, activity} =
CommonAPI.post(user, %{
status: "guys i just don't wanna leave the swamp",
visibility: visibility
})
assert :ok = ParadeDB.add_to_index(activity)
end)
end
test "deletes posts from index when deleted locally" do
user = insert(:user)
ClientMock
|> expect(:query, 2, fn sql, params ->
cond do
String.contains?(sql, "INSERT INTO pleroma_search_documents") ->
send(self(), :inserted)
{:ok, %{}}
String.contains?(sql, "DELETE FROM pleroma_search_documents") ->
assert [object_id] = params
assert is_integer(object_id)
send(self(), :deleted)
{:ok, %{}}
true ->
flunk("Unexpected ParadeDB SQL: #{inspect(sql)}")
end
end)
Config
|> expect(:get, 6, fn
[Pleroma.Search, :module], nil ->
ParadeDB
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
end)
{:ok, activity} =
CommonAPI.post(user, %{
status: "guys i just don't wanna leave the swamp",
visibility: "public"
})
args = %{"op" => "add_to_index", "activity" => activity.id}
assert_enqueued(worker: SearchIndexingWorker, args: args)
assert :ok = perform_job(SearchIndexingWorker, args)
assert_received(:inserted)
{:ok, _} = CommonAPI.delete(activity.id, user)
delete_args = %{"op" => "remove_from_index", "object" => activity.object.id}
assert_enqueued(worker: SearchIndexingWorker, args: delete_args)
assert :ok = perform_job(SearchIndexingWorker, delete_args)
assert_received(:deleted)
end
test "search returns activities in backend order" do
user = insert(:user)
{:ok, activity1} = CommonAPI.post(user, %{status: "first swamp", visibility: "public"})
{:ok, activity2} = CommonAPI.post(user, %{status: "second swamp", visibility: "public"})
activity1_id = activity1.id
activity2_id = activity2.id
{:ok, dumped_activity1_id} = FlakeId.Ecto.CompatType.dump(activity1_id)
{:ok, dumped_activity2_id} = FlakeId.Ecto.CompatType.dump(activity2_id)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "SELECT id FROM pleroma_search_documents"
assert ["swamp", _limit, _offset] = params
{:ok, %{rows: [[dumped_activity2_id], [dumped_activity1_id]]}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
0
end)
assert [%{id: ^activity2_id}, %{id: ^activity1_id}] =
ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
end
test "search excludes non-public activities from backend hits" do
user = insert(:user)
{:ok, public_activity} =
CommonAPI.post(user, %{status: "public swamp", visibility: "public"})
{:ok, private_activity} =
CommonAPI.post(user, %{status: "private swamp", visibility: "private"})
public_activity_id = public_activity.id
{:ok, dumped_public_id} = FlakeId.Ecto.CompatType.dump(public_activity.id)
{:ok, dumped_private_id} = FlakeId.Ecto.CompatType.dump(private_activity.id)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "SELECT id FROM pleroma_search_documents"
assert ["swamp", _limit, _offset] = params
{:ok, %{rows: [[dumped_private_id], [dumped_public_id]]}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
0
end)
assert [%{id: ^public_activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
end
test "search supports backend ids returned as uuid strings" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "uuid swamp", visibility: "public"})
activity_id = activity.id
{:ok, dumped_id} = FlakeId.Ecto.CompatType.dump(activity_id)
{:ok, uuid_string} = Ecto.UUID.load(dumped_id)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "SELECT id FROM pleroma_search_documents"
assert ["swamp", _limit, _offset] = params
{:ok, %{rows: [[uuid_string]]}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
0
end)
assert [%{id: ^activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
end
test "search uses configured fuzzy distance" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "runing shoes", visibility: "public"})
activity_id = activity.id
{:ok, dumped_id} = FlakeId.Ecto.CompatType.dump(activity_id)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "SELECT id FROM pleroma_search_documents"
assert sql =~ "($1::text)::pdb.fuzzy(1)"
assert ["running shoes", _limit, _offset] = params
{:ok, %{rows: [[dumped_id]]}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
1
end)
assert [%{id: ^activity_id}] = ParadeDB.search(nil, "running shoes", limit: 40, offset: 0)
end
end
end

View file

@ -51,7 +51,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
})
Config
|> expect(:get, 3, fn
|> expect(:get, 4, fn
[Pleroma.Search, :module], nil ->
QdrantSearch
@ -93,7 +93,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
})
Config
|> expect(:get, 3, fn
|> expect(:get, 4, fn
[Pleroma.Search, :module], nil ->
QdrantSearch
@ -158,7 +158,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
end)
Config
|> expect(:get, 6, fn
|> expect(:get, 7, fn
[Pleroma.Search, :module], nil ->
QdrantSearch

View file

@ -0,0 +1,154 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.SearchTest do
require Pleroma.Constants
use Pleroma.DataCase, async: true
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Object
alias Pleroma.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.SearchIndexingWorker
test "indexes posts that are public or unlisted" do
user = insert(:user)
Enum.each(["public", "unlisted"], fn visibility ->
{:ok, activity} =
CommonAPI.post(user, %{
status: "Well this is a story all about how my life got flipped turned upside down",
visibility: visibility
})
args = %{"op" => "add_to_index", "activity" => activity.id}
assert_enqueued(worker: SearchIndexingWorker, args: args)
end)
end
test "doesn't index posts that are not public" do
user = insert(:user)
Enum.each(["private", "direct"], fn visibility ->
{:ok, activity} =
CommonAPI.post(user, %{
status: "guys i just don't wanna leave the swamp",
visibility: visibility
})
args = %{"op" => "add_to_index", "activity" => activity.id}
refute_enqueued(worker: SearchIndexingWorker, args: args)
end)
end
test "Indexes appropriate activity types" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "I'm my own hype man",
visibility: "public"
})
args = %{"op" => "add_to_index", "activity" => activity.id}
assert_enqueued(worker: SearchIndexingWorker, args: args)
{:ok, fav_activity} = CommonAPI.favorite(activity.id, user)
args = %{"op" => "add_to_index", "activity" => fav_activity.id}
refute_enqueued(worker: SearchIndexingWorker, args: args)
{:ok, repeat_activity} = CommonAPI.repeat(activity.id, user)
args = %{"op" => "add_to_index", "activity" => repeat_activity.id}
refute_enqueued(worker: SearchIndexingWorker, args: args)
end
test "object_to_search_data includes content warnings and attachment descriptions" do
object = %Object{
id: 1,
data: %{
"id" => "https://example.com/objects/1",
"type" => "Note",
"content" => "body text",
"summary" => "subject text",
"attachment" => [
%{"type" => "Document", "name" => "image description"},
%{
"type" => "Document",
"name" => "remote-image.jpg",
"summary" => "remote image description"
},
%{"type" => "Document", "name" => ""}
],
"published" => DateTime.utc_now() |> DateTime.to_iso8601()
}
}
assert %{content: content} = Search.object_to_search_data(object)
assert content =~ "body text"
assert content =~ "subject text"
assert content =~ "image description"
assert content =~ "remote image description"
refute content =~ "remote-image.jpg"
end
test "object_to_search_data ignores objects with invalid published dates" do
for published <- [nil, 123, "not a date"] do
object = %Object{
id: 1,
data: %{
"id" => "https://example.com/objects/1",
"type" => "Note",
"content" => "body text",
"published" => published
}
}
refute Search.object_to_search_data(object)
end
end
test "indexable? accepts searchable content warnings and attachment descriptions" do
public = Pleroma.Constants.as_public()
published = DateTime.utc_now() |> DateTime.to_iso8601()
for data <- [
%{"content" => ".", "summary" => "subject text", "attachment" => []},
%{
"content" => ".",
"summary" => "",
"attachment" => [%{"type" => "Document", "name" => "image description"}]
}
] do
object = %Object{
id: 1,
data:
Map.merge(data, %{
"id" => "https://example.com/objects/1",
"type" => "Note",
"to" => [public],
"published" => published
})
}
activity = %Pleroma.Activity{
id: Ecto.UUID.generate(),
data: %{"type" => "Create"},
object: object,
recipients: [public]
}
assert Search.indexable?(activity)
end
end
end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.SignatureTest do
import Mock
alias Pleroma.Signature
alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@ -103,6 +104,18 @@ defmodule Pleroma.SignatureTest do
end
end
describe "validate_signature/1" do
test "treats HTTP signature errors as failed validation" do
conn = %Plug.Conn{method: "GET", request_path: "/inbox", req_headers: []}
Mox.expect(HTTPSignaturesMock, :validate_conn, fn _conn ->
{:error, :request_target_header}
end)
assert Signature.validate_signature(conn) == false
end
end
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==

View file

@ -0,0 +1,99 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tesla.Middleware.ConnectionPoolTest do
use Pleroma.DataCase
import Mox
alias Pleroma.Tesla.Middleware.ConnectionPool
defmodule FailingAdapter do
@behaviour Tesla.Adapter
@impl Tesla.Adapter
def call(_env, _opts), do: {:error, :request_failed}
end
defmodule EmptyAdapter do
@behaviour Tesla.Adapter
@impl Tesla.Adapter
def call(env, _opts), do: {:ok, %{env | status: 204, body: ""}}
end
test "discards the connection when the adapter cannot finish a request" do
test_pid = self()
stub(Pleroma.GunMock, :open, fn _, _, _ ->
{:ok, conn} = Task.start_link(fn -> Process.sleep(:infinity) end)
send(test_pid, {:gun_conn, conn})
{:ok, conn}
end)
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
client = Tesla.client([ConnectionPool], FailingAdapter)
assert {:error, :request_failed} =
Tesla.get(client, "http://discard.example/", opts: [adapter: []])
assert_receive {:gun_conn, conn}
assert eventually(fn -> not Process.alive?(conn) end)
end
test "releases a chunks lease when the response has no stream" do
clear_config([:connections_pool, :max_idle_time], 5)
test_pid = self()
stub(Pleroma.GunMock, :open, fn _, _, _ ->
{:ok, conn} = Task.start_link(fn -> Process.sleep(:infinity) end)
send(test_pid, {:gun_conn, conn})
{:ok, conn}
end)
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
client = Tesla.client([ConnectionPool], EmptyAdapter)
assert {:ok, %Tesla.Env{status: 204, body: ""}} =
Tesla.get(client, "http://empty-chunks.example/",
opts: [adapter: [body_as: :chunks]]
)
assert_receive {:gun_conn, conn}
worker = worker_for_conn(conn)
assert eventually(fn -> not Process.alive?(worker) end)
end
test "rejects unmanaged pooled streams" do
client = Tesla.client([ConnectionPool], EmptyAdapter)
assert {:error, :pooled_streaming_requires_chunks} =
Tesla.get(client, "http://stream.example/", opts: [adapter: [body_as: :stream]])
end
defp worker_for_conn(conn) do
[worker] =
Registry.select(Pleroma.Gun.ConnectionPool, [
{{:_, :"$1", {:"$2", :_}}, [{:==, :"$2", conn}], [:"$1"]}
])
worker
end
defp eventually(predicate, attempts \\ 50)
defp eventually(_predicate, 0), do: false
defp eventually(predicate, attempts) do
if predicate.() do
true
else
Process.sleep(10)
eventually(predicate, attempts - 1)
end
end
end

View file

@ -34,6 +34,20 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadataTest do
assert meta.blurhash == "eXJi-E:SwCEm5rCmn$+YWYn+15K#5A$xxCi{SiV]s*W:Efa#s.jE-T"
end
test "it gets dimensions for grayscale images" do
upload = %Pleroma.Upload{
name: "break_analyze.png",
content_type: "image/png",
path: Path.absname("test/fixtures/break_analyze.png"),
tempfile: Path.absname("test/fixtures/break_analyze.png")
}
{:ok, :filtered, meta} = AnalyzeMetadata.filter(upload)
assert %{width: 1410, height: 2048} = meta
assert is_nil(meta.blurhash)
end
test "adds the dimensions for videos" do
upload = %Pleroma.Upload{
name: "coolvideo.mp4",

View file

@ -3,8 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
use Pleroma.DataCase
use Pleroma.DataCase, async: true
import Mox
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
alias Pleroma.Upload
setup do
@ -19,21 +21,26 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
%{upload_file: upload_file}
end
setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
test "it replaces filename on pre-defined text", %{upload_file: upload_file} do
clear_config([Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
ConfigMock
|> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> "custom-file.png" end)
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
assert name == "custom-file.png"
end
test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do
clear_config([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}")
ConfigMock
|> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> "custom-file.{extension}" end)
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
assert name == "custom-file.jpg"
end
test "it replaces filename on random text", %{upload_file: upload_file} do
ConfigMock
|> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> nil end)
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
assert <<_::bytes-size(14)>> <> ".jpg" = name
refute name == "an… image.jpg"

View file

@ -25,8 +25,8 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocationTest do
assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :filtered}
{exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.#{type}"])
{exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.#{type}"])
{exif_original, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010.#{type}"])
{exif_filtered, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010_tmp.#{type}"])
assert String.match?(exif_original, ~r/GPS/)
refute String.match?(exif_filtered, ~r/GPS/)

View file

@ -3,9 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.MogrifunTest do
use Pleroma.DataCase
import Mock
use Pleroma.DataCase, async: true
import Mox
alias Pleroma.MogrifyMock
alias Pleroma.Upload
alias Pleroma.Upload.Filter
@ -22,23 +23,12 @@ defmodule Pleroma.Upload.Filter.MogrifunTest do
tempfile: Path.absname("test/fixtures/image_tmp.jpg")
}
task =
Task.async(fn ->
assert_receive {:apply_filter, {}}, 4_000
end)
MogrifyMock
|> stub(:open, fn _file -> %{} end)
|> stub(:custom, fn _image, _action -> %{} end)
|> stub(:custom, fn _image, _action, _options -> %{} end)
|> stub(:save, fn _image, [in_place: true] -> :ok end)
with_mocks([
{Mogrify, [],
[
open: fn _f -> %Mogrify.Image{} end,
custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end,
custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end,
save: fn _f, _o -> :ok end
]}
]) do
assert Filter.Mogrifun.filter(upload) == {:ok, :filtered}
end
Task.await(task)
assert Filter.Mogrifun.filter(upload) == {:ok, :filtered}
end
end

View file

@ -3,13 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.MogrifyTest do
use Pleroma.DataCase
import Mock
use Pleroma.DataCase, async: true
import Mox
alias Pleroma.MogrifyMock
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
alias Pleroma.Upload.Filter
setup :verify_on_exit!
test "apply mogrify filter" do
clear_config(Filter.Mogrify, args: [{"tint", "40"}])
ConfigMock
|> stub(:get!, fn [Filter.Mogrify, :args] -> [{"tint", "40"}] end)
File.cp!(
"test/fixtures/image.jpg",
@ -23,19 +28,11 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do
tempfile: Path.absname("test/fixtures/image_tmp.jpg")
}
task =
Task.async(fn ->
assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000
end)
MogrifyMock
|> expect(:open, fn _file -> %{} end)
|> expect(:custom, fn _image, "tint", "40" -> %{} end)
|> expect(:save, fn _image, [in_place: true] -> :ok end)
with_mock Mogrify,
open: fn _f -> %Mogrify.Image{} end,
custom: fn _m, _a -> :ok end,
custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end,
save: fn _f, _o -> :ok end do
assert Filter.Mogrify.filter(upload) == {:ok, :filtered}
end
Task.await(task)
assert Filter.Mogrify.filter(upload) == {:ok, :filtered}
end
end

View file

@ -5,12 +5,13 @@
defmodule Pleroma.Upload.FilterTest do
use Pleroma.DataCase
import Mox
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
alias Pleroma.Upload.Filter
setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
test "applies filters" do
clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
ConfigMock
|> stub(:get, fn [Pleroma.Upload.Filter.AnonymizeFilename, :text] -> "custom-file.png" end)
File.cp!(
"test/fixtures/image.jpg",

View file

@ -227,20 +227,35 @@ defmodule Pleroma.UploadTest do
assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg"
end
test "escapes reserved uri characters" do
test "escapes disallowed reserved characters in uri path" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: ":?#[]@!$&\\'()*+,;=.jpg"
filename: ":?#[]@!$&'()*+,;=.jpg"
}
{:ok, data} = Upload.store(file)
[attachment_url | _] = data["url"]
assert Path.basename(attachment_url["href"]) ==
"%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg"
":%3F%23%5B%5D@!$&'()*+,;=.jpg"
end
test "double %-encodes filename" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "file with %20.jpg"
}
{:ok, data} = Upload.store(file)
[attachment_url | _] = data["url"]
assert Path.basename(attachment_url["href"]) == "file%20with%20%2520.jpg"
end
end
@ -267,4 +282,23 @@ defmodule Pleroma.UploadTest do
refute String.starts_with?(url, base_url <> "/media/")
end
end
describe "Setting a link_name for uploaded media" do
setup do: clear_config([Pleroma.Upload, :link_name], true)
test "encodes name parameter in query" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "test file.jpg"
}
{:ok, data} = Upload.store(file)
[attachment_url | _] = data["url"]
assert Path.basename(attachment_url["href"]) == "test%20file.jpg?name=test+file.jpg"
end
end
end

View file

@ -0,0 +1,195 @@
# Pleroma: A lightweight social networking server
# Copyright © Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.SearchTest do
use Pleroma.DataCase, async: false
import Pleroma.Factory
alias Pleroma.Instances
alias Pleroma.Repo
alias Pleroma.User
describe "search/2 mention suggestions" do
test "prioritizes followed/follower users before others" do
user = insert(:user)
related =
insert(:user,
local: false,
nickname: "hj@real.example",
ap_id: "https://real.example/users/hj",
last_status_at: ~N[2020-01-01 00:00:00]
)
other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00])
{:ok, _related, _user} = User.follow(related, user)
results = User.search("hj", for_user: user) |> Enum.map(& &1.id)
assert results == [related.id, other.id]
end
test "orders followed/follower users by most recent activity" do
user = insert(:user)
older =
insert(:user,
local: false,
nickname: "ali@remote.example",
ap_id: "https://remote.example/users/ali",
last_status_at: ~N[2020-01-01 00:00:00]
)
newer =
insert(:user,
local: false,
nickname: "alia@remote.example",
ap_id: "https://remote.example/users/alia",
last_status_at: ~N[2020-01-02 00:00:00]
)
{:ok, _user, _older} = User.follow(user, older)
{:ok, _user, _newer} = User.follow(user, newer)
assert [newer.id, older.id] ==
User.search("ali", for_user: user)
|> Enum.map(& &1.id)
end
test "groups followed/follower users first and sorts them by recency" do
user = insert(:user)
following_newest =
insert(:user,
local: false,
nickname: "mentiontesta@related.example",
ap_id: "https://related.example/users/mentiontesta",
last_status_at: ~N[2020-01-03 00:00:00]
)
follower_middle =
insert(:user,
local: false,
nickname: "mentiontestb@related.example",
ap_id: "https://related.example/users/mentiontestb",
last_status_at: ~N[2020-01-02 00:00:00]
)
mutual_oldest =
insert(:user,
local: false,
nickname: "mentiontestc@related.example",
ap_id: "https://related.example/users/mentiontestc",
last_status_at: ~N[2020-01-01 00:00:00]
)
unrelated_newer =
insert(:user,
local: false,
nickname: "mentiontestd@unrelated.example",
ap_id: "https://unrelated.example/users/mentiontestd",
last_status_at: ~N[2020-01-04 00:00:00]
)
{:ok, _user, _following_newest} = User.follow(user, following_newest)
{:ok, _follower_middle, _user} = User.follow(follower_middle, user)
{:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest)
{:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user)
results = User.search("mentiontest", for_user: user)
assert Enum.map(results, & &1.id) ==
[following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id]
end
test "uses last_active_at when last_status_at is missing" do
user = insert(:user)
older =
insert(:user,
local: false,
nickname: "activefallbacka@remote.example",
ap_id: "https://remote.example/users/activefallbacka",
last_status_at: nil,
last_active_at: ~N[2020-01-01 00:00:00]
)
newer =
insert(:user,
local: false,
nickname: "activefallbackb@remote.example",
ap_id: "https://remote.example/users/activefallbackb",
last_status_at: nil,
last_active_at: ~N[2020-01-02 00:00:00]
)
{:ok, _user, _older} = User.follow(user, older)
{:ok, _user, _newer} = User.follow(user, newer)
assert [newer.id, older.id] ==
User.search("activefallback", for_user: user)
|> Enum.map(& &1.id)
end
test "does not return deactivated users even if related" do
user = insert(:user)
active =
insert(:user,
local: false,
nickname: "deactivatedtesta@remote.example",
ap_id: "https://remote.example/users/deactivatedtesta",
last_status_at: ~N[2020-01-02 00:00:00]
)
deactivated =
insert(:user,
local: false,
nickname: "deactivatedtestb@remote.example",
ap_id: "https://remote.example/users/deactivatedtestb",
last_status_at: ~N[2020-01-03 00:00:00]
)
{:ok, _user, _active} = User.follow(user, active)
{:ok, _user, _deactivated} = User.follow(user, deactivated)
Repo.update!(Ecto.Changeset.change(deactivated, is_active: false))
results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id)
assert results == [active.id]
end
test "does not return users from unreachable instances" do
user = insert(:user)
{:ok, _instance} = Instances.set_unreachable("dead.example")
dead =
insert(:user,
local: false,
nickname: "ali@dead.example",
ap_id: "https://dead.example/users/ali",
last_status_at: ~N[2020-01-02 00:00:00]
)
alive =
insert(:user,
local: false,
nickname: "ali@alive.example",
ap_id: "https://alive.example/users/ali",
last_status_at: ~N[2020-01-02 00:00:00]
)
{:ok, _user, _alive} = User.follow(user, alive)
{:ok, _user, _dead} = User.follow(user, dead)
results = User.search("ali", for_user: user) |> Enum.map(& &1.id)
assert results == [alive.id]
end
end
end

View file

@ -3,11 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserRelationshipTest do
alias Pleroma.DateTimeMock
alias Pleroma.UserRelationship
use Pleroma.DataCase, async: false
use Pleroma.DataCase, async: true
import Mock
import Mox
import Pleroma.Factory
describe "*_exists?/2" do
@ -52,6 +53,9 @@ defmodule Pleroma.UserRelationshipTest do
end
test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do
DateTimeMock
|> stub_with(Pleroma.DateTime.Impl)
for relationship_type <- [
:block,
:mute,
@ -80,13 +84,15 @@ defmodule Pleroma.UserRelationshipTest do
end
test "if record already exists, returns it", %{users: [user1, user2]} do
user_block =
with_mock NaiveDateTime, [:passthrough], utc_now: fn -> ~N[2017-03-17 17:09:58] end do
{:ok, %{inserted_at: ~N[2017-03-17 17:09:58]}} =
UserRelationship.create_block(user1, user2)
end
fixed_datetime = ~N[2017-03-17 17:09:58]
assert user_block == UserRelationship.create_block(user1, user2)
Pleroma.DateTimeMock
|> expect(:utc_now, 2, fn -> fixed_datetime end)
{:ok, %{inserted_at: ^fixed_datetime}} = UserRelationship.create_block(user1, user2)
# Test the idempotency without caring about the exact time
assert {:ok, _} = UserRelationship.create_block(user1, user2)
end
end

View file

@ -366,5 +366,13 @@ defmodule Pleroma.UserSearchTest do
assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
end
test "find users accepting chat messages only" do
user1 = insert(:user, nickname: "user1", accepts_chat_messages: true)
insert(:user, nickname: "user2", accepts_chat_messages: false)
[found_user1] = User.search("user", capabilities: ["accepts_chat_messages"])
assert found_user1.id == user1.id
end
end
end

View file

@ -11,16 +11,18 @@ defmodule Pleroma.UserTest do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Webhook.Notify
use Pleroma.DataCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import ExUnit.CaptureLog
import Mock
import Swoosh.TestAssertions
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
@ -719,6 +721,14 @@ defmodule Pleroma.UserTest do
assert user.is_confirmed
end
test_with_mock "triggers webhooks", Notify, trigger_webhooks: fn _, _ -> nil end do
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
assert_called(Notify.trigger_webhooks(registered_user, :"account.created"))
end
end
describe "user registration, with :account_activation_required" do
@ -876,17 +886,17 @@ defmodule Pleroma.UserTest do
describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
test "for mastodon" do
ap_id = "a@mastodon.example"
{:ok, fetched_user} = User.get_or_fetch(ap_id)
test "fetches a mastodon split-domain nickname" do
nickname = "a@mastodon.example"
{:ok, fetched_user} = User.get_or_fetch(nickname)
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
assert fetched_user.nickname == "a@mastodon.example"
end
test "for pleroma" do
ap_id = "a@pleroma.example"
{:ok, fetched_user} = User.get_or_fetch(ap_id)
test "fetches a pleroma split-domain nickname" do
nickname = "a@pleroma.example"
{:ok, fetched_user} = User.get_or_fetch(nickname)
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
assert fetched_user.nickname == "a@pleroma.example"
@ -936,6 +946,89 @@ defmodule Pleroma.UserTest do
assert fetched_user == "not found nonexistent"
end
test "does not rename an existing remote actor from rogue WebFinger data" do
clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
actor_id = "https://legit-actor.example/users/alice"
Tesla.Mock.mock(fn
%{url: "https://evil-webfinger.example/.well-known/host-meta"} ->
{:ok, %Tesla.Env{status: 404}}
%{
url:
"https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example"
} ->
Tesla.Mock.json(%{
"subject" => "acct:claimed@evil-webfinger.example",
"links" => [
%{
"rel" => "self",
"type" => "application/activity+json",
"href" => actor_id
}
]
})
%{url: ^actor_id} ->
{:ok,
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => actor_id,
"type" => "Person",
"preferredUsername" => "alice",
"name" => "Alice",
"summary" => "",
"inbox" => "https://legit-actor.example/users/alice/inbox",
"outbox" => "https://legit-actor.example/users/alice/outbox",
"followers" => "https://legit-actor.example/users/alice/followers",
"following" => "https://legit-actor.example/users/alice/following"
})
}}
%{url: "https://legit-actor.example/.well-known/host-meta"} ->
{:ok, %Tesla.Env{status: 404}}
%{
url:
"https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example"
} ->
Tesla.Mock.json(%{
"subject" => "acct:alice@legit-actor.example",
"links" => [
%{
"rel" => "self",
"type" => "application/activity+json",
"href" => actor_id
}
]
})
end)
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
refute User.get_by_ap_id(actor_id)
refute User.get_by_nickname("claimed@evil-webfinger.example")
orig_user =
insert(:user,
local: false,
nickname: "alice@legit-actor.example",
ap_id: actor_id
)
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example")
assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example"
refute User.get_by_nickname("claimed@evil-webfinger.example")
end
test "updates an existing user, if stale" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
@ -1881,6 +1974,11 @@ defmodule Pleroma.UserTest do
end
end
test "get_or_fetch_public_key_for_ap_id fetches a user that's not in the db" do
assert {:ok, _key} =
User.get_or_fetch_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
test "get_public_key_for_ap_id returns correctly for user that's not in the db" do
assert :error = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
@ -1900,14 +1998,26 @@ defmodule Pleroma.UserTest do
end
describe "caching" do
test "set_cache stores friends under the AP ID cache key" do
user = insert(:user)
User.set_cache(user)
assert {:ok, []} = Cachex.get(:user_cache, "friends_ap_ids:#{user.ap_id}")
assert {:ok, nil} = Cachex.get(:user_cache, "friends_ap_ids:#{user.nickname}")
end
test "invalidate_cache works" do
user = insert(:user)
User.set_cache(user)
User.get_cached_by_id(user.id)
User.invalidate_cache(user)
{:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
{:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
{:ok, nil} = Cachex.get(:user_cache, "friends_ap_ids:#{user.ap_id}")
{:ok, nil} = Cachex.get(:user_cache, "id:#{user.id}")
end
test "User.delete() plugs any possible zombie objects" do
@ -2405,8 +2515,8 @@ defmodule Pleroma.UserTest do
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
follower_address: "https://remote.org/users/masto_closed/followers",
following_address: "https://remote.org/users/masto_closed/following"
)
assert other_user.following_count == 0
@ -2426,8 +2536,8 @@ defmodule Pleroma.UserTest do
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
follower_address: "https://remote.org/users/masto_closed/followers",
following_address: "https://remote.org/users/masto_closed/following"
)
assert other_user.following_count == 0
@ -2447,8 +2557,8 @@ defmodule Pleroma.UserTest do
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
follower_address: "https://remote.org/users/masto_closed/followers",
following_address: "https://remote.org/users/masto_closed/following"
)
assert other_user.following_count == 0
@ -2669,8 +2779,12 @@ defmodule Pleroma.UserTest do
assert {:ok, user} = User.update_last_active_at(user)
assert user.last_active_at >= test_started_at
assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq]
assert NaiveDateTime.compare(
user.last_active_at,
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
) in [:lt, :eq]
last_active_at =
NaiveDateTime.utc_now()
@ -2682,10 +2796,15 @@ defmodule Pleroma.UserTest do
|> cast(%{last_active_at: last_active_at}, [:last_active_at])
|> User.update_and_set_cache()
assert user.last_active_at == last_active_at
assert NaiveDateTime.compare(user.last_active_at, last_active_at) == :eq
assert {:ok, user} = User.update_last_active_at(user)
assert user.last_active_at >= test_started_at
assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq]
assert NaiveDateTime.compare(
user.last_active_at,
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
) in [:lt, :eq]
end
test "active_user_count/1" do
@ -2783,6 +2902,15 @@ defmodule Pleroma.UserTest do
assert user_updated.also_known_as |> length() == 1
assert user2.ap_id in user_updated.also_known_as
end
test "should tolerate non-http(s) aliases" do
user =
insert(:user, %{
also_known_as: ["at://did:plc:xgvzy7ni6ig6ievcbls5jaxe"]
})
assert "at://did:plc:xgvzy7ni6ig6ievcbls5jaxe" in user.also_known_as
end
end
describe "alias_users/1" do
@ -2919,4 +3047,74 @@ defmodule Pleroma.UserTest do
assert [%{"verified_at" => ^verified_at}] = user.fields
end
describe "follow_hashtag/2" do
test "should follow a hashtag" do
user = insert(:user)
hashtag = insert(:hashtag)
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.followed_hashtags |> Enum.count() == 1
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
end
test "should not follow a hashtag twice" do
user = insert(:user)
hashtag = insert(:hashtag)
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.followed_hashtags |> Enum.count() == 1
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
end
test "can follow multiple hashtags" do
user = insert(:user)
hashtag = insert(:hashtag)
other_hashtag = insert(:hashtag)
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
assert {:ok, _} = user |> User.follow_hashtag(other_hashtag)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.followed_hashtags |> Enum.count() == 2
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
assert other_hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
end
end
describe "unfollow_hashtag/2" do
test "should unfollow a hashtag" do
user = insert(:user)
hashtag = insert(:hashtag)
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.followed_hashtags |> Enum.count() == 0
end
test "should not error when trying to unfollow a hashtag twice" do
user = insert(:user)
hashtag = insert(:hashtag)
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.followed_hashtags |> Enum.count() == 0
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
alias Pleroma.Webhook.Notify
import ExUnit.CaptureLog
import Mock
@ -415,6 +416,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"
@ -465,6 +565,69 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
test "works with avatar/banner href as list" do
user_id = "https://queef.in/cute_cat"
user_data =
"test/fixtures/users_mock/href_as_array.json"
|> File.read!()
|> Jason.decode!()
|> Map.delete("featured")
|> Jason.encode!()
Tesla.Mock.mock(fn
%{
method: :get,
url: ^user_id
} ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert length(user.avatar["url"]) == 1
assert length(user.banner["url"]) == 1
assert user.avatar["url"] |> List.first() |> Map.fetch!("href") ==
"https://queef.in/storage/profile.webp"
assert user.banner["url"] |> List.first() |> Map.fetch!("href") ==
"https://queef.in/storage/banner.gif"
end
test "works with alsoKnownAs as string" do
user_id = "https://hub.netzgemeinde.eu/channel/jupiter_rowland"
user_data =
"test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json"
|> File.read!()
user_data_decoded =
user_data
|> Jason.decode!()
Tesla.Mock.mock(fn
%{
method: :get,
url: ^user_id
} ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert is_list(user.also_known_as)
assert user.also_known_as == [user_data_decoded["alsoKnownAs"]]
end
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
@ -826,12 +989,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert object.data["repliesCount"] == 2
end
test "increates quotes count", %{user: user} do
test "increases quotes count", %{user: user} do
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"})
ap_id = activity.data["id"]
quote_data = %{status: "1", quote_id: activity.id}
quote_data = %{status: "1", quoted_status_id: activity.id}
# public
{:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "public"))
@ -867,6 +1030,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
describe "fetch activities for followed hashtags" do
test "it should return public activities that reference a given hashtag" do
hashtag = insert(:hashtag, name: "tenshi")
user = insert(:user)
other_user = insert(:user)
{:ok, normally_visible} =
CommonAPI.post(other_user, %{status: "hello :)", visibility: "public"})
{:ok, public} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "public"})
{:ok, _unrelated} = CommonAPI.post(user, %{status: "dai #tensh", visibility: "public"})
{:ok, unlisted} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "unlisted"})
{:ok, _private} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "private"})
activities =
ActivityPub.fetch_activities([other_user.follower_address], %{
followed_hashtags: [hashtag.id]
})
assert length(activities) == 3
normal_id = normally_visible.id
public_id = public.id
unlisted_id = unlisted.id
assert [%{id: ^normal_id}, %{id: ^public_id}, %{id: ^unlisted_id}] = activities
end
end
describe "fetch activities in context" do
test "retrieves activities that have a given context" do
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
@ -1243,6 +1433,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert activity == expected_activity
end
test "includes only reblogs on request" do
user = insert(:user)
{:ok, _} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
[activity] = ActivityPub.fetch_user_activities(user, nil, %{only_reblogs: true})
assert activity == expected_activity
end
describe "irreversible filters" do
setup do
user = insert(:user)
@ -1424,8 +1624,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
%{test_file: test_file}
end
test "strips / from filename", %{test_file: file} do
file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"}
test "strips / from filename", %{test_file: %Plug.Upload{} = file} do
file = %{file | filename: "../../../../../nested/bad.jpg"}
{:ok, %Object{} = object} = ActivityPub.upload(file)
[%{"href" => href}] = object.data["url"]
assert Regex.match?(~r"/bad.jpg$", href)
@ -1654,32 +1854,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
} = activity
end
test_with_mock "strips status data from Flag, before federating it",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
object_ap_id: object_ap_id,
content: content
},
Utils,
[:passthrough],
[] do
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
new_data = put_in(activity.data, ["object"], [target_account.ap_id, object_ap_id])
assert_called(Utils.maybe_federate(%{activity | data: new_data}))
end
test_with_mock "reverts on error",
%{
reporter: reporter,
@ -1704,18 +1878,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Repo.aggregate(Object, :count, :id) == 1
assert Repo.aggregate(Notification, :count, :id) == 0
end
test_with_mock "triggers webhooks",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
content: content
},
Notify,
[:passthrough],
trigger_webhooks: fn _, _ -> nil end do
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
assert_called(Notify.trigger_webhooks(activity, :"report.created"))
end
end
test "fetch_activities/2 returns activities addressed to a list " do
user = insert(:user)
member = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
{:ok, list} = Pleroma.List.follow(list, member)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
{:ok, %Activity{} = activity} =
CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
activity = Repo.preload(activity, :bookmark)
activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
activity = %{activity | thread_muted?: !!activity.thread_muted?}
assert ActivityPub.fetch_activities([], %{user: user}) == [activity]
end
@ -1758,8 +1956,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following"
follower_address: "https://remote.org/users/fuser2/followers",
following_address: "https://remote.org/users/fuser2/following"
)
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
@ -1770,7 +1968,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
test "detects hidden followers" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/followers?page=1" ->
"https://remote.org/users/masto_closed/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
@ -1781,8 +1979,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
follower_address: "https://remote.org/users/masto_closed/followers",
following_address: "https://remote.org/users/masto_closed/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
@ -1793,7 +1991,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
test "detects hidden follows" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/following?page=1" ->
"https://remote.org/users/masto_closed/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
@ -1804,8 +2002,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
follower_address: "https://remote.org/users/masto_closed/followers",
following_address: "https://remote.org/users/masto_closed/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
@ -1817,8 +2015,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
follower_address: "http://localhost:8080/followers/fuser3",
following_address: "http://localhost:8080/following/fuser3"
follower_address: "https://remote.org/followers/fuser3",
following_address: "https://remote.org/following/fuser3"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
@ -1831,28 +2029,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
test "doesn't crash when follower and following counters are hidden" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_hidden_counters/following" ->
"https://remote.org/users/masto_hidden_counters/following" ->
json(
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/followers"
"id" => "https://remote.org/users/masto_hidden_counters/followers"
},
headers: HttpRequestMock.activitypub_object_headers()
)
"http://localhost:4001/users/masto_hidden_counters/following?page=1" ->
"https://remote.org/users/masto_hidden_counters/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
"http://localhost:4001/users/masto_hidden_counters/followers" ->
"https://remote.org/users/masto_hidden_counters/followers" ->
json(
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/following"
"id" => "https://remote.org/users/masto_hidden_counters/following"
},
headers: HttpRequestMock.activitypub_object_headers()
)
"http://localhost:4001/users/masto_hidden_counters/followers?page=1" ->
"https://remote.org/users/masto_hidden_counters/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
end
end)
@ -1860,8 +2058,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_hidden_counters/followers",
following_address: "http://localhost:4001/users/masto_hidden_counters/following"
follower_address: "https://remote.org/users/masto_hidden_counters/followers",
following_address: "https://remote.org/users/masto_hidden_counters/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
@ -1915,7 +2113,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert User.following?(follower, old_user)
assert User.following?(follower_move_opted_out, old_user)
assert {:ok, activity} = ActivityPub.move(old_user, new_user)
assert {:ok, %Activity{} = activity} = ActivityPub.move(old_user, new_user)
assert %Activity{
actor: ^old_ap_id,
@ -1947,7 +2145,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert User.following?(follower_move_opted_out, old_user)
refute User.following?(follower_move_opted_out, new_user)
activity = %Activity{activity | object: nil}
activity = %{activity | object: nil}
assert [%Notification{activity: ^activity}] = Notification.for_user(follower)
@ -2743,4 +2941,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

@ -1,117 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.FODirectReplyTest do
use Pleroma.DataCase
import Pleroma.Factory
require Pleroma.Constants
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.MRF.FODirectReply
alias Pleroma.Web.CommonAPI
test "replying to followers-only/private is changed to direct" do
batman = insert(:user, nickname: "batman")
robin = insert(:user, nickname: "robin")
{:ok, post} =
CommonAPI.post(batman, %{
status: "Has anyone seen Selina Kyle's latest selfies?",
visibility: "private"
})
reply = %{
"type" => "Create",
"actor" => robin.ap_id,
"to" => [batman.ap_id, robin.follower_address],
"cc" => [],
"object" => %{
"type" => "Note",
"actor" => robin.ap_id,
"content" => "@batman 🤤 ❤️ 🐈‍⬛",
"to" => [batman.ap_id, robin.follower_address],
"cc" => [],
"inReplyTo" => Object.normalize(post).data["id"]
}
}
expected_to = [batman.ap_id]
expected_cc = []
assert {:ok, filtered} = FODirectReply.filter(reply)
assert expected_to == filtered["to"]
assert expected_cc == filtered["cc"]
assert expected_to == filtered["object"]["to"]
assert expected_cc == filtered["object"]["cc"]
end
test "replies to unlisted posts are unmodified" do
batman = insert(:user, nickname: "batman")
robin = insert(:user, nickname: "robin")
{:ok, post} =
CommonAPI.post(batman, %{
status: "Has anyone seen Selina Kyle's latest selfies?",
visibility: "unlisted"
})
reply = %{
"type" => "Create",
"actor" => robin.ap_id,
"to" => [batman.ap_id, robin.follower_address],
"cc" => [],
"object" => %{
"type" => "Note",
"actor" => robin.ap_id,
"content" => "@batman 🤤 ❤️ 🐈<200d>⬛",
"to" => [batman.ap_id, robin.follower_address],
"cc" => [],
"inReplyTo" => Object.normalize(post).data["id"]
}
}
assert {:ok, filtered} = FODirectReply.filter(reply)
assert match?(^filtered, reply)
end
test "replies to public posts are unmodified" do
batman = insert(:user, nickname: "batman")
robin = insert(:user, nickname: "robin")
{:ok, post} =
CommonAPI.post(batman, %{status: "Has anyone seen Selina Kyle's latest selfies?"})
reply = %{
"type" => "Create",
"actor" => robin.ap_id,
"to" => [batman.ap_id, robin.follower_address],
"cc" => [],
"object" => %{
"type" => "Note",
"actor" => robin.ap_id,
"content" => "@batman 🤤 ❤️ 🐈<200d>⬛",
"to" => [batman.ap_id, robin.follower_address],
"cc" => [],
"inReplyTo" => Object.normalize(post).data["id"]
}
}
assert {:ok, filtered} = FODirectReply.filter(reply)
assert match?(^filtered, reply)
end
test "non-reply posts are unmodified" do
batman = insert(:user, nickname: "batman")
{:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"})
assert {:ok, filtered} = FODirectReply.filter(post)
assert match?(^filtered, post)
end
end

View file

@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["object"]["sensitive"]
end
@ -94,7 +94,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
refute modified["object"]["sensitive"]
end

View file

@ -109,4 +109,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do
{:ok, filtered} = InlineQuotePolicy.filter(activity)
assert filtered == activity
end
# Mastodon uses p tags instead of span in their quote posts
# URLs in quoteUri and post content are already mismatched
test "skips objects which already have an .inline-quote p" do
object = File.read!("test/fixtures/quote_post/mastodon_quote_post.json") |> Jason.decode!()
# Normally the ObjectValidator will fix this before it reaches MRF
object = Map.put(object, "quoteUrl", object["quoteUri"])
activity = %{
"type" => "Create",
"actor" => "https://mastodon.social/users/gwynnion",
"object" => object
}
{:ok, filtered} = InlineQuotePolicy.filter(activity)
assert filtered == activity
end
end

View file

@ -54,14 +54,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
setup do: clear_config([:media_proxy, :enabled], true)
test "it prefetches media proxy URIs" do
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
{:ok, %Tesla.Env{status: 200, body: ""}}
end)
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
with_mock HTTP,
get: fn _, _, opts ->
send(self(), {:prefetch_opts, opts})
{:ok, []}
end do
MediaProxyWarmingPolicy.filter(@message)
assert called(HTTP.get(:_, :_, :_))
assert_receive {:prefetch_opts, opts}
refute Keyword.has_key?(opts, :follow_redirect)
refute Keyword.has_key?(opts, :force_redirect)
end
end
@ -81,10 +84,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
end
test "history-aware" do
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
{:ok, %Tesla.Env{status: 200, body: ""}}
end)
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history)
@ -93,10 +92,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
end
test "works with Updates" do
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
{:ok, %Tesla.Env{status: 200, body: ""}}
end)
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history |> Map.put("type", "Update"))

View file

@ -39,15 +39,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReplyTest do
}
}
expected_to = [batman.ap_id, robin.follower_address]
expected_cc = [Pleroma.Constants.as_public()]
assert {:ok, filtered} = QuietReply.filter(reply)
assert expected_to == filtered["to"]
assert expected_cc == filtered["cc"]
assert expected_to == filtered["object"]["to"]
assert expected_cc == filtered["object"]["cc"]
assert batman.ap_id in filtered["to"]
assert batman.ap_id in filtered["object"]["to"]
assert robin.follower_address in filtered["to"]
assert robin.follower_address in filtered["object"]["to"]
assert Pleroma.Constants.as_public() in filtered["cc"]
assert Pleroma.Constants.as_public() in filtered["object"]["cc"]
end
test "replying to unlisted post is unmodified" do

View file

@ -1,5 +1,5 @@
defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicyTest do
use Pleroma.DataCase, async: true
use Pleroma.DataCase, async: false
alias Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy

View file

@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
assert File.exists?(fullpath)
end
test "rejects invalid shortcodes", %{path: path} do
test "rejects invalid shortcodes with slashes", %{path: path} do
message = %{
"type" => "Create",
"object" => %{
@ -113,6 +113,58 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
refute File.exists?(fullpath)
end
test "rejects invalid shortcodes with dots", %{path: path} do
message = %{
"type" => "Create",
"object" => %{
"emoji" => [{"fired.fox", "https://example.org/emoji/firedfox"}],
"actor" => "https://example.org/users/admin"
}
}
fullpath = Path.join(path, "fired.fox.png")
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
refute "fired.fox" in installed()
refute File.exists?(path)
assert {:ok, _message} = StealEmojiPolicy.filter(message)
refute "fired.fox" in installed()
refute File.exists?(fullpath)
end
test "rejects invalid shortcodes with special characters", %{path: path} do
message = %{
"type" => "Create",
"object" => %{
"emoji" => [{"fired:fox", "https://example.org/emoji/firedfox"}],
"actor" => "https://example.org/users/admin"
}
}
fullpath = Path.join(path, "fired:fox.png")
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
refute "fired:fox" in installed()
refute File.exists?(path)
assert {:ok, _message} = StealEmojiPolicy.filter(message)
refute "fired:fox" in installed()
refute File.exists?(fullpath)
end
test "reject regex shortcode", %{message: message} do
refute "firedfox" in installed()
@ -171,5 +223,74 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
refute "firedfox" in installed()
end
test "accepts valid alphanum shortcodes", %{path: path} do
message = %{
"type" => "Create",
"object" => %{
"emoji" => [{"fire1fox", "https://example.org/emoji/fire1fox.png"}],
"actor" => "https://example.org/users/admin"
}
}
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire1fox.png"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
refute "fire1fox" in installed()
refute File.exists?(path)
assert {:ok, _message} = StealEmojiPolicy.filter(message)
assert "fire1fox" in installed()
end
test "accepts valid shortcodes with underscores", %{path: path} do
message = %{
"type" => "Create",
"object" => %{
"emoji" => [{"fire_fox", "https://example.org/emoji/fire_fox.png"}],
"actor" => "https://example.org/users/admin"
}
}
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire_fox.png"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
refute "fire_fox" in installed()
refute File.exists?(path)
assert {:ok, _message} = StealEmojiPolicy.filter(message)
assert "fire_fox" in installed()
end
test "accepts valid shortcodes with hyphens", %{path: path} do
message = %{
"type" => "Create",
"object" => %{
"emoji" => [{"fire-fox", "https://example.org/emoji/fire-fox.png"}],
"actor" => "https://example.org/users/admin"
}
}
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire-fox.png"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
refute "fire-fox" in installed()
refute File.exists?(path)
assert {:ok, _message} = StealEmojiPolicy.filter(message)
assert "fire-fox" in installed()
end
defp installed, do: Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
end

View file

@ -10,18 +10,25 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
alias Pleroma.Web.ActivityPub.MRF
defp regexes_match!([], []), do: true
defp regexes_match!([authority | authority_rest], [checked | checked_rest]) do
authority.source == checked.source and regexes_match!(authority_rest, checked_rest)
end
defp regexes_match!(_, _), do: false
test "subdomains_regex/1" do
assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [
~r/^unsafe.tld$/i,
~r/^(.*\.)*unsafe.tld$/i
]
regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"])
assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^(.*\.)*unsafe.tld$/i])
end
describe "subdomain_match/2" do
test "common domains" do
regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"])
assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i]
assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i])
assert MRF.subdomain_match?(regexes, "unsafe.tld")
assert MRF.subdomain_match?(regexes, "unsafe2.tld")
@ -32,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
test "wildcard domains with one subdomain" do
regexes = MRF.subdomains_regex(["*.unsafe.tld"])
assert regexes == [~r/^(.*\.)*unsafe.tld$/i]
assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i])
assert MRF.subdomain_match?(regexes, "unsafe.tld")
assert MRF.subdomain_match?(regexes, "sub.unsafe.tld")
@ -43,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
test "wildcard domains with two subdomains" do
regexes = MRF.subdomains_regex(["*.unsafe.tld"])
assert regexes == [~r/^(.*\.)*unsafe.tld$/i]
assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i])
assert MRF.subdomain_match?(regexes, "unsafe.tld")
assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld")
@ -54,7 +61,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
test "matches are case-insensitive" do
regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"])
assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i]
assert regexes_match!(regexes, [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i])
assert MRF.subdomain_match?(regexes, "UNSAFE.TLD")
assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD")

View file

@ -86,23 +86,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidationTest do
object = Object.normalize(post_activity, fetch: false)
# Another user can't announce it
{:ok, announce, []} = Builder.announce(announcer, object, public: false)
{:ok, announce, []} = Builder.announce(announcer, object, visibility: "private")
{:error, cng} = ObjectValidator.validate(announce, [])
assert {:actor, {"can not announce this object", []}} in cng.errors
# The actor of the object can announce it
{:ok, announce, []} = Builder.announce(user, object, public: false)
# The actor of the object can announce it with a restrictive scope
{:ok, announce, []} = Builder.announce(user, object, visibility: "private")
assert {:ok, _, _} = ObjectValidator.validate(announce, [])
{:ok, announce, []} = Builder.announce(user, object, visibility: "direct")
assert {:ok, _, _} = ObjectValidator.validate(announce, [])
# The actor of the object can not announce it publicly
{:ok, announce, []} = Builder.announce(user, object, public: true)
{:ok, announce, []} = Builder.announce(user, object, visibility: "public")
{:error, cng1} = ObjectValidator.validate(announce, [])
{:error, cng} = ObjectValidator.validate(announce, [])
{:ok, announce, []} = Builder.announce(user, object, visibility: "unlisted")
{:error, cng2} = ObjectValidator.validate(announce, [])
assert {:actor, {"can not announce this object publicly", []}} in cng.errors
{:ok, announce, []} = Builder.announce(user, object, visibility: "local")
{:error, cng3} = ObjectValidator.validate(announce, [])
for cng <- [cng1, cng2, cng3] do
assert {:actor, {"can not announce this object publicly", []}} in cng.errors
end
end
end
end

View file

@ -5,12 +5,33 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do
use Pleroma.DataCase, async: true
alias Pleroma.Language.LanguageDetectorMock
alias Pleroma.StaticStubbedConfigMock
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
alias Pleroma.Web.ActivityPub.Utils
import Mox
import Pleroma.Factory
# Setup for all tests
setup do
# 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)
|> stub(:detect, fn _text -> nil end)
:ok
end
describe "Notes" do
setup do
user = insert(:user)
@ -46,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
{:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"})
{:ok, %{"object" => external_rep}} =
Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data)
Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data)
%{external_rep: external_rep}
end
@ -128,6 +149,182 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a Misskey MFM note is rendered from source content" do
user = insert(:user, ap_id: "https://misskey.example/users/alice")
note = %{
"id" => "https://misskey.example/notes/1",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => "$[spin.speed=1s mfm goes here] <script>alert('xss')</script>",
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert source["mediaType"] == "text/x.misskeymarkdown"
assert content =~ ~s(class="mfm-spin")
assert content =~ ~s(data-mfm-speed="1s")
assert content =~ "mfm goes here"
refute content =~ "original content"
refute content =~ "<script"
end
test "a Misskey MFM note resolves only cached AP mention tags" do
remote_user = insert(:user, ap_id: "https://misskey.example/users/carol")
local_user = insert(:user, nickname: "local_user")
note = %{
"id" => "https://misskey.example/notes/3",
"type" => "Note",
"actor" => remote_user.ap_id,
"attributedTo" => remote_user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"tag" => [
%{
"type" => "Mention",
"name" => "@local_user",
"href" => local_user.ap_id
},
%{
"type" => "Mention",
"name" => "@uncached",
"href" => "https://misskey.example/users/uncached"
}
],
"source" => %{
"content" => "@local_user @uncached $[spin hello]",
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content =~ local_user.ap_id
assert content =~ "@uncached"
end
test "a Misskey MFM note drops oversized source content instead of parsing it" do
user = insert(:user, ap_id: "https://misskey.example/users/oversized")
note = %{
"id" => "https://misskey.example/notes/4",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "<span class=\"mfm-spin\">safe fallback</span>",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content == "<span class=\"mfm-spin\">safe fallback</span>"
refute Map.has_key?(source, "content")
end
test "a note drops oversized non-MFM source content" do
user = insert(:user, ap_id: "https://example.com/users/source")
note = %{
"id" => "https://example.com/notes/1",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "regular content",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/markdown"
}
}
%{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note)
assert source == %{"mediaType" => "text/markdown"}
end
test "a Misskey MFM note with legacy _misskey_content is rendered" do
user = insert(:user, ap_id: "https://misskey.example/users/legacy")
note = %{
"id" => "https://misskey.example/notes/5",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"_misskey_content" => "$[spin legacy]"
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"}
assert content =~ ~s(class="mfm-spin")
assert content =~ "legacy"
end
test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do
user = insert(:user, ap_id: "https://misskey.example/users/bob")
note = %{
"id" => "https://misskey.example/notes/2",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" =>
"<span class=\"mfm-spin\">already rendered</span><script>alert('xss')</script>",
"htmlMfm" => true,
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content == "<span class=\"mfm-spin\">already rendered</span>alert(&#39;xss&#39;)"
refute Map.has_key?(source, "content")
end
test "a Note with validated likes collection validates" do
insert(:user, ap_id: "https://pol.social/users/mkljczk")
%{"object" => note} =
"test/fixtures/mastodon-update-with-likes.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "Fedibird quote post" do
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
@ -176,4 +373,102 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
name: "RE: https://server.example/objects/123"
}
end
describe "Note language" do
test "it detects language from JSON-LD context" do
user = insert(:user)
note_activity = %{
"@context" => ["https://www.w3.org/ns/activitystreams", %{"@language" => "pl"}],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Szczęść Boże",
"attributedTo" => user.ap_id
},
"actor" => user.ap_id
}
{:ok, _create_activity, meta} = ObjectValidator.validate(note_activity, [])
assert meta[:object_data]["language"] == "pl"
end
test "it detects language from contentMap" do
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Szczęść Boże",
"contentMap" => %{
"de" => "Gott segne",
"pl" => "Szczęść Boże"
},
"attributedTo" => user.ap_id
}
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
assert object.language == "pl"
end
test "it doesn't call LanguageDetector when language is specified" do
# Set up expectation that detect should not be called
LanguageDetectorMock
|> expect(:detect, 0, fn _ -> flunk("LanguageDetector.detect should not be called") end)
|> stub(:missing_dependencies, fn -> [] end)
|> stub(:configured?, fn -> true end)
# Stub the StaticStubbedConfigMock to return our mock for the provider
StaticStubbedConfigMock
|> stub(:get, fn
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
_other -> nil
end)
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "a post in English",
"contentMap" => %{
"en" => "a post in English"
},
"attributedTo" => user.ap_id
}
ArticleNotePageValidator.cast_and_apply(note)
end
test "it adds contentMap if language is specified" do
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "тест",
"language" => "uk",
"attributedTo" => user.ap_id
}
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
assert object.contentMap == %{
"uk" => "тест"
}
end
end
end

View file

@ -13,6 +13,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do
import Pleroma.Factory
describe "attachments" do
test "works with apng" do
attachment =
%{
"mediaType" => "image/apng",
"name" => "",
"type" => "Document",
"url" =>
"https://media.misskeyusercontent.com/io/2859c26e-cd43-4550-848b-b6243bc3fe28.apng"
}
assert {:ok, attachment} =
AttachmentValidator.cast_and_validate(attachment)
|> Ecto.Changeset.apply_action(:insert)
assert attachment.mediaType == "image/apng"
end
test "fails without url" do
attachment = %{
"mediaType" => "",

View file

@ -59,4 +59,37 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidatorTest do
assert validated.valid?
assert {:context, note["context"]} in validated.changes
end
test "a Create/Note without addressing falls back to the Note's recipients" do
user = insert(:user)
note = %{
"id" => Utils.generate_object_id(),
"type" => "Note",
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => [],
"content" => "Hello world",
"context" => Utils.generate_context_id()
}
note_activity = %{
"id" => Utils.generate_activity_id(),
"type" => "Create",
"actor" => note["actor"],
"object" => note,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => Utils.generate_context_id()
}
# Build metadata
{:ok, object_data} = ObjectValidator.cast_and_apply(note_activity["object"])
meta = [object_data: ObjectValidator.stringify_keys(object_data)]
validated = CreateGenericValidator.cast_and_validate(note_activity, meta)
assert validated.valid?
assert Ecto.Changeset.get_field(validated, :to) == note["to"]
assert Ecto.Changeset.get_field(validated, :cc) == note["cc"]
end
end

View file

@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
end
test "returns an error if the object can't be updated by the actor", %{
test "returns an error if the object can't be updated by the actor (different domain)", %{
valid_update: valid_update
} do
other_user = insert(:user, local: false)
@ -41,27 +41,72 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
assert {:error, _cng} = ObjectValidator.validate(update, [])
end
test "validates as long as the object is same-origin with the actor", %{
test "returns an error if the object can't be updated by the actor (same domain)", %{
user: user,
valid_update: valid_update
} do
other_user = insert(:user)
user_ap_id = user.ap_id
user_domain = URI.parse(user_ap_id).host
other_user = insert(:user, local: false, domain: user_domain)
update =
valid_update
|> Map.put("actor", other_user.ap_id)
assert {:ok, _update, []} = ObjectValidator.validate(update, [])
assert {:error, _cng} = ObjectValidator.validate(update, [])
end
test "validates if the object is not of an Actor type" do
note = insert(:note)
test "validates if the object is not of an Actor type", %{user: user} do
note = insert(:note, user: user)
updated_note = note.data |> Map.put("content", "edited content")
other_user = insert(:user)
{:ok, update, _} = Builder.update(other_user, updated_note)
{:ok, update, _} = Builder.update(user, updated_note)
assert {:ok, _update, _} = ObjectValidator.validate(update, [])
end
test "returns an error if the remote update target is unknown" do
remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
update = %{
"type" => "Update",
"actor" => remote_user.ap_id,
"id" => "https://example.com/activities/update-unknown-object",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://example.com/objects/unknown",
"actor" => remote_user.ap_id,
"content" => "edited content",
"published" => "2024-07-25T13:33:31Z",
"updated" => "2024-07-25T13:34:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
refute cng.valid?
assert Keyword.has_key?(cng.errors, :object)
end
test "returns an error if the remote update target IRI is unknown" do
remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
update = %{
"type" => "Update",
"actor" => remote_user.ap_id,
"id" => "https://example.com/activities/update-unknown-object-iri",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => "https://example.com/objects/unknown-iri"
}
assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
refute cng.valid?
assert Keyword.has_key?(cng.errors, :object)
end
end
describe "update note" do
@ -133,7 +178,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
user = insert(:user)
{:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"})
{:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"})
{:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data)
{:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data)
%{external_rep: external_rep}
end

View file

@ -6,16 +6,15 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.Web.ConnCase
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
@as_public "https://www.w3.org/ns/activitystreams#Public"
@ -167,122 +166,10 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
})
|> Publisher.publish_one()
end
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
Instances,
[:passthrough],
[] do
_actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
activity = insert(:note_activity)
assert {:ok, _} =
Publisher.prepare_one(%{
inbox: inbox,
activity_id: activity.id,
unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.to_string()
})
|> Publisher.publish_one()
assert called(Instances.set_reachable(inbox))
end
test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
Instances,
[:passthrough],
[] do
_actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
activity = insert(:note_activity)
assert {:ok, _} =
Publisher.prepare_one(%{
inbox: inbox,
activity_id: activity.id,
unreachable_since: nil
})
|> Publisher.publish_one()
refute called(Instances.set_reachable(inbox))
end
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
Instances,
[:passthrough],
[] do
_actor = insert(:user)
inbox = "http://404.site/users/nick1/inbox"
activity = insert(:note_activity)
assert {:cancel, _} =
Publisher.prepare_one(%{inbox: inbox, activity_id: activity.id})
|> Publisher.publish_one()
assert called(Instances.set_unreachable(inbox))
end
test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
Instances,
[:passthrough],
[] do
_actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
activity = insert(:note_activity)
assert capture_log(fn ->
assert {:error, _} =
Publisher.prepare_one(%{
inbox: inbox,
activity_id: activity.id
})
|> Publisher.publish_one()
end) =~ "connrefused"
assert called(Instances.set_unreachable(inbox))
end
test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
Instances,
[:passthrough],
[] do
_actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
activity = insert(:note_activity)
assert {:ok, _} =
Publisher.prepare_one(%{inbox: inbox, activity_id: activity.id})
|> Publisher.publish_one()
refute called(Instances.set_unreachable(inbox))
end
test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
Instances,
[:passthrough],
[] do
_actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
activity = insert(:note_activity)
assert capture_log(fn ->
assert {:error, _} =
Publisher.prepare_one(%{
inbox: inbox,
activity_id: activity.id,
unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.to_string()
})
|> Publisher.publish_one()
end) =~ "connrefused"
refute called(Instances.set_unreachable(inbox))
end
end
describe "publish/2" do
test_with_mock "doesn't publish a non-public activity to quarantined instances.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "doesn't publish a non-public activity to quarantined instances." do
Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}])
follower =
@ -317,10 +204,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
)
end
test_with_mock "Publishes a non-public activity to non-quarantined instances.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "Publishes a non-public activity to non-quarantined instances." do
Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])
follower =
@ -356,10 +240,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
)
end
test_with_mock "Publishes to directly addressed actors with higher priority.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "Publishes to directly addressed actors with higher priority." do
note_activity = insert(:direct_note_activity)
actor = Pleroma.User.get_by_ap_id(note_activity.data["actor"])
@ -368,21 +249,58 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert res == :ok
assert called(
Publisher.enqueue_one(
%{
inbox: :_,
activity_id: note_activity.id
},
priority: 0
)
)
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"op" => "publish_one",
"params" => %{"activity_id" => note_activity.id}
},
priority: 0
)
end
test_with_mock "publishes an activity with BCC to all relevant peers.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "Publishes with the new actor if prepare_activity changes the actor." do
mock(fn
%{method: :post, url: "https://domain.com/users/nick1/inbox", body: body} ->
{:ok, %Tesla.Env{status: 200, body: body}}
end)
other_user =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user)
replaced_actor = insert(:user)
note_activity =
insert(:note_activity,
user: actor,
data_attrs: %{"to" => [other_user.ap_id]}
)
Pleroma.Web.ActivityPub.TransmogrifierMock
|> Mox.expect(:prepare_activity, fn data ->
{:ok, Map.put(data, "actor", replaced_actor.ap_id)}
end)
prepared =
Publisher.prepare_one(%{
inbox: "https://domain.com/users/nick1/inbox",
activity_id: note_activity.id,
cc: ["https://domain.com/users/nick2/inbox"]
})
{:ok, decoded} = Jason.decode(prepared.json)
assert decoded["actor"] == replaced_actor.ap_id
{:ok, published} = Publisher.publish_one(prepared)
sent_activity = Jason.decode!(published.body)
assert sent_activity["actor"] == replaced_actor.ap_id
end
test "publishes an activity with BCC to all relevant peers." do
follower =
insert(:user, %{
local: false,
@ -414,10 +332,34 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
)
end
test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.",
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
test "activity with BCC is published to a list member." do
actor = insert(:user)
{:ok, list} = Pleroma.List.create(%{title: "list"}, actor)
list_member = insert(:user, %{local: false})
Pleroma.List.follow(list, list_member)
note_activity =
insert(:note_activity,
# recipients: [follower.ap_id],
data_attrs: %{"bcc" => [list.ap_id]}
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"params" => %{
inbox: list_member.inbox,
activity_id: note_activity.id
}
}
)
end
test "publishes a delete activity to peers who signed fetch requests to the create acitvity/object." do
fetcher =
insert(:user,
local: false,
@ -520,4 +462,144 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert decoded["cc"] == []
end
test "unlisted activities retain public address in cc" do
user = insert(:user)
# simulate unlistd activity by only having
# public address in cc
activity =
insert(:note_activity,
user: user,
data_attrs: %{
"cc" => [@as_public],
"to" => [user.follower_address]
}
)
assert @as_public in activity.data["cc"]
prepared =
Publisher.prepare_one(%{
inbox: "https://remote.instance/users/someone/inbox",
activity_id: activity.id
})
{:ok, decoded} = Jason.decode(prepared.json)
assert @as_public in decoded["cc"]
# maybe we also have another inbox in cc
# during Publishing
activity =
insert(:note_activity,
user: user,
data_attrs: %{
"cc" => [@as_public],
"to" => [user.follower_address]
}
)
prepared =
Publisher.prepare_one(%{
inbox: "https://remote.instance/users/someone/inbox",
activity_id: activity.id,
cc: ["https://remote.instance/users/someone_else/inbox"]
})
{:ok, decoded} = Jason.decode(prepared.json)
assert decoded["cc"] == [@as_public, "https://remote.instance/users/someone_else/inbox"]
end
test "public address in cc parameter is preserved" do
user = insert(:user)
cc_with_public = [@as_public, "https://example.org/users/other"]
activity =
insert(:note_activity,
user: user,
data_attrs: %{
"cc" => cc_with_public,
"to" => [user.follower_address]
}
)
assert @as_public in activity.data["cc"]
prepared =
Publisher.prepare_one(%{
inbox: "https://remote.instance/users/someone/inbox",
activity_id: activity.id,
cc: cc_with_public
})
{:ok, decoded} = Jason.decode(prepared.json)
assert cc_with_public == decoded["cc"]
end
test "cc parameter is preserved" do
user = insert(:user)
activity =
insert(:note_activity,
user: user,
data_attrs: %{
"cc" => ["https://example.com/specific/user"],
"to" => [user.follower_address]
}
)
prepared =
Publisher.prepare_one(%{
inbox: "https://remote.instance/users/someone/inbox",
activity_id: activity.id,
cc: ["https://example.com/specific/user"]
})
{:ok, decoded} = Jason.decode(prepared.json)
assert decoded["cc"] == ["https://example.com/specific/user"]
end
describe "prepare_one/1 with reporter anonymization" do
test "signs with the anonymized actor keys when Transmogrifier changes actor" do
Pleroma.SignatureMock
|> Mox.stub(:signed_date, fn -> Pleroma.Signature.signed_date() end)
|> Mox.expect(:sign, fn %Pleroma.User{} = user, _headers ->
send(self(), {:signed_as, user.ap_id})
"TESTSIG"
end)
placeholder = insert(:user)
reporter = insert(:user)
target_account = insert(:user)
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
{:ok, reported} = CommonAPI.post(target_account, %{status: "content"})
context = Utils.generate_context_id()
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported],
content: "reason"
})
_prepared =
Publisher.prepare_one(%{
inbox: "http://remote.example/users/alice/inbox",
activity_id: activity.id
})
assert_received {:signed_as, ap_id}
assert ap_id == placeholder.ap_id
end
end
end

View file

@ -784,13 +784,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
{:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"})
{:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true)
{:ok, announce_data, _meta} = Builder.announce(user, post.object, visibility: "public")
{:ok, private_announce_data, _meta} =
Builder.announce(user, private_post.object, public: false)
Builder.announce(user, private_post.object, visibility: "private")
{:ok, relay_announce_data, _meta} =
Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true)
Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object,
visibility: "public"
)
{:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true)
{:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true)

View file

@ -72,7 +72,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do
|> Kernel.put_in(["object", "to"], user.ap_id)
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, data} = Transmogrifier.prepare_activity(activity.data)
assert data["object"]["type"] == "Note"
end

View file

@ -1,14 +1,46 @@
defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiTagBuildingTest do
use Pleroma.DataCase, async: true
alias Pleroma.Web.ActivityPub.Transmogrifier
test "it encodes the id to be a valid url" do
name = "hanapog"
url = "https://misskey.local.live/emojis/hana pog.png"
tag = Transmogrifier.build_emoji_tag({name, url})
tag = Pleroma.Emoji.build_emoji_tag({name, url})
assert tag["id"] == "https://misskey.local.live/emojis/hana%20pog.png"
end
test "it does not double-encode already encoded urls" do
name = "hanapog"
url = "https://misskey.local.live/emojis/hana%20pog.png"
tag = Pleroma.Emoji.build_emoji_tag({name, url})
assert tag["id"] == url
end
test "it encodes disallowed path characters" do
name = "hanapog"
url = "https://example.com/emojis/hana[pog].png"
tag = Pleroma.Emoji.build_emoji_tag({name, url})
assert tag["id"] == "https://example.com/emojis/hana%5Bpog%5D.png"
end
test "local_url does not decode percent in filenames" do
url = Pleroma.Emoji.local_url("/emoji/hana%20pog.png")
assert url == Pleroma.Web.Endpoint.url() <> "/emoji/hana%2520pog.png"
tag = Pleroma.Emoji.build_emoji_tag({"hanapog", url})
assert tag["id"] == url
end
test "local_url encodes question marks in filenames" do
url = Pleroma.Emoji.local_url("/emoji/file?name.png")
assert url == Pleroma.Web.Endpoint.url() <> "/emoji/file%3Fname.png"
end
end

View file

@ -0,0 +1,98 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunAttachmentsTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_data!(data) do
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
defp ingest_fixture!(rel_path) do
rel_path
|> File.read!()
|> Jason.decode!()
|> ingest_data!()
end
test "drops non-http(s) attachment href" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/attachment_payto_link.json")
assert object.data["attachment"] == []
end
test "drops embedded non-attachment objects" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/attachment_embedded_note.json")
assert object.data["attachment"] == []
end
test "accepts attachment url as list and keeps the first link" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/attachment_url_list.json")
[attachment] = object.data["attachment"]
[%{"href" => href} | _] = attachment["url"]
assert href == "https://fedidev.fun/images/007.png"
end
test "drops malformed attachment list entries" do
data = File.read!("test/fixtures/fedidev.fun/attachment_url_list.json") |> Jason.decode!()
data = update_in(data, ["object", "attachment"], &["malformed" | List.wrap(&1)])
{_activity, object} = ingest_data!(data)
assert length(object.data["attachment"]) == 1
end
test "accepts a URI string in an attachment url list" do
data = File.read!("test/fixtures/fedidev.fun/attachment_url_list.json") |> Jason.decode!()
data =
put_in(data, ["object", "attachment", "url"], [
"https://fedidev.fun/images/007.png"
])
{_activity, object} = ingest_data!(data)
assert [%{"url" => [%{"href" => "https://fedidev.fun/images/007.png"}]}] =
object.data["attachment"]
end
end

View file

@ -0,0 +1,103 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunEmojiTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_data!(data) do
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
defp ingest_fixture!(rel_path) do
rel_path
|> File.read!()
|> Jason.decode!()
|> ingest_data!()
end
test "ingests Emoji tag with string icon" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_string.json")
assert object.data["emoji"]["cow"] == "https://fedidev.fun/static/cow.png"
end
test "ingests Emoji tag with icon missing type" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json")
assert object.data["emoji"]["cow2"] == "https://fedidev.fun/static/cow.png"
end
test "ignores Emoji tag missing icon" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_missing_icon.json")
assert object.data["emoji"] == %{}
end
test "ignores Emoji tag missing a string name" do
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
data = put_in(data, ["object", "tag", Access.at(0), "name"], nil)
{_activity, object} = ingest_data!(data)
assert object.data["emoji"] == %{}
end
test "ignores Emoji tag with a non-http icon" do
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
data = put_in(data, ["object", "tag", Access.at(0), "icon"], "file:///tmp/cow.png")
{_activity, object} = ingest_data!(data)
assert object.data["emoji"] == %{}
end
test "normalizes a list-typed Emoji tag before building the emoji map" do
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
data =
put_in(data, ["object", "tag", Access.at(0), "type"], [
"https://example.com/ns#CustomTag",
"Emoji"
])
{_activity, object} = ingest_data!(data)
assert object.data["emoji"]["cow"] == "https://fedidev.fun/static/cow.png"
end
end

View file

@ -0,0 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunInReplyToTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "normalizes Link inReplyTo to a string" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_link.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "normalizes list inReplyTo to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_list.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "drops invalid map shapes in inReplyTo" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_collection.json")
assert object.data["inReplyTo"] == nil
end
test "accepts embedded objects in inReplyTo" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_embedded_note.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
end

View file

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunMissingAttributedToTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
test "inherits attributedTo from surrounding activity" do
data = File.read!("test/fixtures/fedidev.fun/missing_attributed_to.json") |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert object.data["attributedTo"] == data["actor"]
assert object.data["actor"] == data["actor"]
end
end

View file

@ -0,0 +1,64 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunObjectTypeListTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "accepts object type as a list and stores the first type" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/object_type_list.json")
assert object.data["type"] == "Note"
end
test "selects a supported type after an extension type" do
data = File.read!("test/fixtures/fedidev.fun/object_type_list.json") |> Jason.decode!()
data = put_in(data, ["object", "type"], ["https://example.com/ns#CustomType", "Note"])
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert object.data["type"] == "Note"
end
end

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunQuoteUrlListTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "normalizes quoteUri list to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/quote_uri_list.json")
assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "normalizes quoteUrl list to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/quote_url_list.json")
assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "normalizes _misskey_quote list to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/misskey_quote_list.json")
assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap"
end
end

View file

@ -0,0 +1,112 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunTagInferenceTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "infers Hashtag type when missing" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json")
assert "test" in Object.hashtags(object)
end
test "infers Mention type when missing" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/tag_mention_missing_type.json")
assert Enum.any?(object.data["tag"], fn
%{"type" => "Mention", "href" => "https://remote.example/users/bob"} -> true
_ -> false
end)
end
test "normalizes a list-typed tag type" do
data =
File.read!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json") |> Jason.decode!()
data = put_in(data, ["object", "tag", Access.at(0), "type"], ["Hashtag"])
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert "test" in Object.hashtags(object)
end
test "ignores a list-typed Hashtag without a string name" do
data =
File.read!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json") |> Jason.decode!()
data = put_in(data, ["object", "tag", Access.at(0), "type"], ["Hashtag"])
data = put_in(data, ["object", "tag", Access.at(0), "name"], nil)
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert Object.hashtags(object) == []
end
test "infers a Link before deriving quoteUrl" do
data =
File.read!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json") |> Jason.decode!()
data =
put_in(data, ["object", "tag"], [
%{
"href" => "https://fedidev.fun/assets/note1.jsonap",
"mediaType" => "application/activity+json"
}
])
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap"
end
end

View file

@ -0,0 +1,53 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunUrlListTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "normalizes object url list to a string" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/url_list.json")
assert object.data["url"] == "https://fedidev.fun/objects/123"
end
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
use Pleroma.DataCase, async: true
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
@ -75,4 +76,107 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
assert activity_data["object"] == activity.data["object"]
assert activity_data["content"] == "⭐"
end
test "it works for misskey likes with custom emoji" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
data =
File.read!("test/fixtures/misskey-custom-emoji-like.json")
|> Jason.decode!()
|> Map.put("object", activity.data["object"])
_actor = insert(:user, ap_id: data["actor"], local: false)
{:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
assert activity_data["actor"] == data["actor"]
assert activity_data["type"] == "EmojiReact"
assert activity_data["id"] == data["id"]
assert activity_data["object"] == activity.data["object"]
assert activity_data["content"] == ":blobwtfnotlikethis:"
assert [["blobwtfnotlikethis", _, _]] =
Object.get_by_ap_id(activity.data["object"])
|> Object.get_emoji_reactions()
end
test "it works for mitra likes with custom emoji" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
data =
File.read!("test/fixtures/mitra-custom-emoji-like.json")
|> Jason.decode!()
|> Map.put("object", activity.data["object"])
_actor = insert(:user, ap_id: data["actor"], local: false)
{:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
assert activity_data["actor"] == data["actor"]
assert activity_data["type"] == "EmojiReact"
assert activity_data["id"] == data["id"]
assert activity_data["object"] == activity.data["object"]
assert activity_data["content"] == ":ablobcatheartsqueeze:"
assert [["ablobcatheartsqueeze", _, _]] =
Object.get_by_ap_id(activity.data["object"])
|> Object.get_emoji_reactions()
end
test "it works for likes with wrong content" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
data =
File.read!("test/fixtures/mitra-custom-emoji-like.json")
|> Jason.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("content", 1)
_actor = insert(:user, ap_id: data["actor"], local: false)
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
assert activity.data["type"] == "Like"
end
test "it changes incoming dislikes into emoji reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
data =
File.read!("test/fixtures/friendica-dislike.json")
|> Jason.decode!()
|> Map.put("object", activity.data["object"])
_actor = insert(:user, ap_id: data["actor"], local: false)
{:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
refute Enum.empty?(activity.recipients)
assert data["actor"] == "https://my-place.social/profile/vaartis"
assert data["type"] == "EmojiReact"
assert data["content"] == "👎"
assert data["id"] == "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182"
assert data["object"] == activity.data["object"]
data =
File.read!("test/fixtures/friendica-dislike-undo.json")
|> Jason.decode!()
|> put_in(["object", "object"], activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "https://my-place.social/profile/vaartis"
assert data["type"] == "Undo"
assert data["object"] ==
"https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182"
end
end

View file

@ -219,6 +219,36 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
"<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
end
test "it only uses contentMap if content is not present" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Hi",
"contentMap" => %{
"de" => "Hallo",
"uk" => "Привіт"
},
"inReplyTo" => nil,
"attributedTo" => user.ap_id
},
"actor" => user.ap_id
}
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(message)
object = Object.normalize(data["object"], fetch: false)
assert object.data["content"] == "Hi"
end
test "it works for incoming notices with a nil contentMap (firefish)" do
data =
File.read!("test/fixtures/mastodon-post-activity-contentmap.json")
@ -478,7 +508,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
{:ok, activity} = Transmogrifier.handle_incoming(message)
{:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, _} = Transmogrifier.prepare_activity(activity.data)
end
test "successfully reserializes a message with AS2 objects in IR" do
@ -507,7 +537,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
{:ok, activity} = Transmogrifier.handle_incoming(message)
{:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, _} = Transmogrifier.prepare_activity(activity.data)
end
end
@ -542,11 +572,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
object_with_reply = Map.put(data["object"], "inReplyTo", [])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == []
assert modified_object["inReplyTo"] == nil
end
test "returns modified object when allowed incoming reply", %{data: data} do
@ -641,13 +671,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
test "returns object with emoji when object contains list tags" do
assert Transmogrifier.fix_emoji(%{
"tag" => [
%{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
%{
"type" => "Emoji",
"name" => ":bib:",
"icon" => %{"url" => "https://example.com/test"}
},
%{"type" => "Hashtag"}
]
}) == %{
"emoji" => %{"bib" => "/test"},
"emoji" => %{"bib" => "https://example.com/test"},
"tag" => [
%{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
%{
"icon" => %{"url" => "https://example.com/test"},
"name" => ":bib:",
"type" => "Emoji"
},
%{"type" => "Hashtag"}
]
}
@ -655,10 +693,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
test "returns object with emoji when object contains map tag" do
assert Transmogrifier.fix_emoji(%{
"tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
"tag" => %{
"type" => "Emoji",
"name" => ":bib:",
"icon" => %{"url" => "https://example.com/test"}
}
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
"emoji" => %{"bib" => "https://example.com/test"},
"tag" => %{
"icon" => %{"url" => "https://example.com/test"},
"name" => ":bib:",
"type" => "Emoji"
}
}
end
end
describe "fix_tag/1" do
test "ignores malformed Hashtag names and tag entries" do
tag = %{"type" => "Hashtag", "name" => 123}
assert Transmogrifier.fix_tag(%{"tag" => [tag, "malformed"]}) == %{
"tag" => [tag, "malformed"]
}
end
end
@ -666,12 +722,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
describe "set_replies/1" do
setup do: clear_config([:activitypub, :note_replies_output_limit], 2)
test "returns unmodified object if activity doesn't have self-replies" do
test "still provides reply collection id even if activity doesn't have replies yet" do
data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.set_replies(data) == data
object = data["object"] |> Map.delete("replies")
modified = Transmogrifier.set_replies(object)
refute object["replies"]
assert modified["replies"]
assert match?(%{"id" => "http" <> _, "totalItems" => 0}, modified["replies"])
# first page should be omitted if there are no entries anyway
refute modified["replies"]["first"]
end
test "sets `replies` collection with a limited number of self-replies" do
test "sets `replies` collection with a limited number of replies, preferring oldest" do
[user, another_user] = insert_list(2, :user)
{:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"})
@ -700,7 +763,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
object = Object.normalize(activity, fetch: false)
replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
assert %{"type" => "Collection", "items" => ^replies_uris} =
assert %{"type" => "OrderedCollection", "first" => %{"orderedItems" => ^replies_uris}} =
Transmogrifier.set_replies(object.data)["replies"]
end
end

View file

@ -170,4 +170,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do
assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
end
test "it displays voters count for a poll" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "???",
poll: %{expires_in: 10, options: ["yes", "no"]}
})
object = Object.normalize(activity, fetch: false)
{:ok, _, _} = CommonAPI.vote(object, other_user, [1])
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
refute Map.has_key?(modified["object"], "voters")
assert modified["object"]["votersCount"] == 1
end
end

View file

@ -50,6 +50,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
assert user.bio == "<p>Some bio</p>"
end
test "it selects an actor type after an extension type" do
user = insert(:user, local: false)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
update_data["object"]
|> Map.put("actor", user.ap_id)
|> Map.put("id", user.ap_id)
|> Map.put("type", ["https://example.com/ns#CustomActor", "Person"])
update_data =
update_data
|> Map.put("actor", user.ap_id)
|> Map.put("object", object)
assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(update_data)
assert User.get_cached_by_ap_id(user.ap_id).name == "gargle"
end
test "it works with alsoKnownAs" do
%{ap_id: actor} = insert(:user, local: false)

View file

@ -9,7 +9,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
@ -83,6 +86,43 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity.data["cc"] == [user.ap_id]
end
test "it rejects Flag activities when both reporter and reported account are remote" do
reporter = insert(:user, local: false, domain: "mastodon.cat")
reported = insert(:user, local: false, domain: "nicecrew.digital")
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => reporter.ap_id,
"content" => "blocked AND reported!!!",
"object" => [reported.ap_id, "https://nicecrew.digital/objects/report-status"],
"type" => "Flag"
}
assert {:reject, reason} = Transmogrifier.handle_incoming(message)
assert reason =~ "third-party report"
refute "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
end
test "it accepts Flag activities with just actor id as object" do
user = insert(:user)
other_user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"cc" => [user.ap_id],
"object" => user.ap_id,
"type" => "Flag",
"content" => "blocked AND reported!!!",
"actor" => other_user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert activity.data["content"] == "blocked AND reported!!!"
assert activity.data["actor"] == other_user.ap_id
assert activity.data["cc"] == [user.ap_id]
end
test "it accepts Move activities" do
old_user = insert(:user)
new_user = insert(:user)
@ -123,6 +163,30 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity.data["context"] == object.data["context"]
end
test "it fixes the public scope addressing" do
insert(:user,
ap_id: "https://mymath.rocks/endpoints/SYn3cl_N4HAPfPHgo2x37XunLEmhV9LnxCggcYwyec0"
)
object =
"test/fixtures/bovine-bogus-public-note.json"
|> File.read!()
|> Jason.decode!()
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => "https://mymath.rocks/endpoints/SYn3cl_N4HAPfPHgo2x37XunLEmhV9LnxCggcYwyec0",
"object" => object
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
object = Object.normalize(activity, fetch: false)
assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
assert "https://www.w3.org/ns/activitystreams#Public" in object.data["to"]
end
test "it keeps link tags" do
insert(:user, ap_id: "https://example.org/users/alice")
@ -156,6 +220,246 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
# It fetched the quoted post
assert Object.normalize("https://misskey.io/notes/8vs6wxufd0")
end
test "doesn't allow remote edits to fake local likes" do
# as a spot check for no internal fields getting injected
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
local_user = insert(:user)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => [local_user.ap_id]
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
end
test "strips internal fields from history items in edited notes" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
local_user = insert(:user)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => [],
"like_count" => 0
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [
Map.merge(create_data["object"], %{
"likes" => [local_user.ap_id],
"like_count" => 1,
"pleroma" => %{"internal_field" => "should_be_stripped"}
})
]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
# Check that internal fields are stripped from history items
history_item = List.first(object.data["formerRepresentations"]["orderedItems"])
assert history_item["likes"] == []
assert history_item["like_count"] == 0
refute Map.has_key?(history_item, "pleroma")
end
test "doesn't trip over remote likes in notes" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => %{
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097/likes",
"totalItems" => 0,
"type" => "Collection"
}
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "likes", "totalItems"], 666)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
# in the future this should retain remote likes, but for now:
assert object.data["like_count"] == 0
end
test "doesn't trip over remote likes in polls" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Question",
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "vote!",
"anyOf" => [
%{
"type" => "Note",
"name" => "a",
"replies" => %{
"type" => "Collection",
"totalItems" => 3
}
},
%{
"type" => "Note",
"name" => "b",
"replies" => %{
"type" => "Collection",
"totalItems" => 1
}
}
],
"likes" => %{
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073/likes",
"totalItems" => 0,
"type" => "Collection"
}
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "vote now!")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "likes", "totalItems"], 666)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "vote!"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "vote now!"
assert object.data["likes"] == []
# in the future this should retain remote likes, but for now:
assert object.data["like_count"] == 0
end
end
describe "prepare outgoing" do
@ -166,7 +470,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, announce_activity} = CommonAPI.repeat(activity.id, user)
{:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(announce_activity.data)
assert modified["object"]["content"] == "hey"
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
@ -181,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
with_mock Pleroma.Notification,
get_notified_from_activity: fn _, _ -> [] end do
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
object = modified["object"]
@ -207,7 +511,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["@context"] == Utils.make_json_ld_header()["@context"]
@ -218,7 +522,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
@ -234,7 +538,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
"name" => "#2hu"
}
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["object"]["tag"] == [expected_tag]
end
@ -257,7 +561,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
url: "https://pleroma.social"
} == activity.object.data["generator"]
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert length(modified["object"]["tag"]) == 2
@ -266,12 +570,15 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["generator"])
assert is_nil(modified["object"]["rules"])
assert is_nil(modified["object"]["language"])
assert is_nil(modified["object"]["voters"])
end
test "it strips internal fields of article" do
activity = insert(:article_activity)
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert length(modified["object"]["tag"]) == 2
@ -288,13 +595,13 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == false
{:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == false
@ -304,18 +611,18 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
visibility: "direct"
})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == true
end
test "it strips BCC field" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert is_nil(modified["bcc"])
end
@ -323,7 +630,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it can handle Listen activities" do
listen_activity = insert(:listen)
{:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
# This has an inlined object as in ObjectView
{:ok, modified} = Transmogrifier.prepare_activity(listen_activity.data)
assert modified["type"] == "Listen"
@ -331,7 +639,36 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
{:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data)
user_ap_id = user.ap_id
activity_ap_id = activity.data["id"]
activity_to = activity.data["to"]
activity_cc = activity.data["cc"]
object_ap_id = activity.data["object"]
object_type = activity.object.data["type"]
# This does not have an inlined object
{:ok, modified2} = Transmogrifier.prepare_activity(activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "Listen",
"actor" => ^user_ap_id,
"to" => ^activity_to,
"cc" => ^activity_cc,
"context" => "http://localhost" <> _,
"id" => ^activity_ap_id,
"object" => %{
"actor" => ^user_ap_id,
"attributedTo" => ^user_ap_id,
"id" => ^object_ap_id,
"type" => ^object_type,
"to" => ^activity_to,
"cc" => ^activity_cc
}
},
modified2
)
end
test "custom emoji urls are URI encoded" do
@ -340,7 +677,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
{:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, prepared} = Transmogrifier.prepare_activity(activity.data)
assert length(prepared["object"]["tag"]) == 1
@ -355,7 +692,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
{:ok, update} = CommonAPI.update(activity, user, %{status: "mew mew :blank:"})
{:ok, prepared} = Transmogrifier.prepare_outgoing(update.data)
{:ok, prepared} = Transmogrifier.prepare_activity(update.data)
assert %{
"content" => "mew mew :blank:",
@ -371,19 +708,204 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
} = prepared["object"]
end
test "Updates of Actors are handled" do
user = insert(:user, local: true)
changeset = User.update_changeset(user, %{name: "new name"})
{:ok, unpersisted_user} = Ecto.Changeset.apply_action(changeset, :update)
updated_object =
UserView.render("user.json", user: unpersisted_user)
|> Map.delete("@context")
{:ok, update_data, []} = Builder.update(user, updated_object)
{:ok, activity, _} =
Pipeline.common_pipeline(update_data,
local: true,
user_update_changeset: changeset
)
assert {:ok, prepared} = Transmogrifier.prepare_activity(activity.data)
assert prepared["type"] == "Update"
assert prepared["@context"]
assert prepared["object"]["type"] == user.actor_type
end
test "Correctly handles Undo activities" do
blocked = insert(:user)
blocker = insert(:user, local: true)
blocked_ap_id = blocked.ap_id
blocker_ap_id = blocker.ap_id
{:ok, %Activity{} = block_activity} = CommonAPI.block(blocked, blocker)
{:ok, %Activity{} = undo_activity} = CommonAPI.unblock(blocked, blocker)
{:ok, data} = Transmogrifier.prepare_activity(undo_activity.data)
block_ap_id = block_activity.data["id"]
assert is_binary(block_ap_id)
assert match?(
%{
"@context" => [_ | _],
"type" => "Undo",
"id" => "http://localhost" <> _,
"actor" => ^blocker_ap_id,
"object" => ^block_ap_id,
"to" => [^blocked_ap_id],
"cc" => [],
"bto" => [],
"bcc" => []
},
data
)
end
test "Correctly handles EmojiReact activities" do
user = insert(:user, local: true)
note_activity = insert(:note_activity)
user_ap_id = user.ap_id
user_followers = user.follower_address
note_author = note_activity.data["actor"]
note_ap_id = note_activity.data["object"]
assert is_binary(note_author)
assert is_binary(note_ap_id)
{:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, "🐈")
{:ok, data} = Transmogrifier.prepare_activity(react_activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "EmojiReact",
"actor" => ^user_ap_id,
"to" => [^user_followers, ^note_author],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"bto" => [],
"bcc" => [],
"content" => "🐈",
"context" => "2hu",
"id" => "http://localhost" <> _,
"object" => ^note_ap_id,
"tag" => []
},
data
)
end
test "EmojiReact custom emoji urls are URI encoded" do
user = insert(:user, local: true)
note_activity = insert(:note_activity)
{:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, ":dinosaur:")
{:ok, data} = Transmogrifier.prepare_activity(react_activity.data)
assert length(data["tag"]) == 1
tag = List.first(data["tag"])
url = tag["icon"]["url"]
assert url == "http://localhost:4001/emoji/dino%20walking.gif"
assert tag["id"] == "http://localhost:4001/emoji/dino%20walking.gif"
end
test "it prepares a quote post" do
user = insert(:user)
{:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"})
{:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quote_id: quoted_post.id})
{:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quoted_status_id: quoted_post.id})
{:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data)
{:ok, modified} = Transmogrifier.prepare_activity(quote_post.data)
%{data: %{"id" => quote_id}} = Object.normalize(quoted_post)
assert modified["object"]["quoteUrl"] == quote_id
assert modified["object"]["quoteUri"] == quote_id
end
test "it adds language of the object to its json-ld context" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.object.data)
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
end
test "it adds language of the object to Create activity json-ld context" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
end
test "it strips report data" do
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, reported_activity} = CommonAPI.post(target_account, %{status: content})
context = Utils.generate_context_id()
object_ap_id = reported_activity.object.data["id"]
assert {:ok, activity} =
Pleroma.Web.ActivityPub.ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
{:ok, data} = Transmogrifier.prepare_activity(activity.data)
expected_data =
activity.data
|> put_in(["object"], [target_account.ap_id, object_ap_id])
|> Map.put("actor", reporter.ap_id)
|> Map.merge(Utils.make_json_ld_header())
assert data == expected_data
end
test "it strips report data and anonymize" do
placeholder = insert(:user)
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, reported_activity} = CommonAPI.post(target_account, %{status: content})
context = Utils.generate_context_id()
object_ap_id = reported_activity.object.data["id"]
assert {:ok, activity} =
Pleroma.Web.ActivityPub.ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
{:ok, data} = Transmogrifier.prepare_activity(activity.data)
expected_data =
activity.data
|> put_in(["object"], [target_account.ap_id, object_ap_id])
|> Map.put("actor", placeholder.ap_id)
|> Map.merge(Utils.make_json_ld_header())
assert data == expected_data
end
end
describe "actor rewriting" do
@ -621,5 +1143,14 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
processed = Transmogrifier.prepare_object(original)
assert processed["formerRepresentations"] == original["formerRepresentations"]
end
test "it uses contentMap to specify post language" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
object = Transmogrifier.prepare_object(activity.object.data)
assert %{"contentMap" => %{"pl" => "Cześć"}} = object
end
end
end

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UtilsTest do
use Pleroma.DataCase, async: true
use Pleroma.DataCase, async: false
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
@ -173,16 +173,32 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
end
end
test "make_json_ld_header/0" do
assert Utils.make_json_ld_header() == %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
}
]
}
describe "make_json_ld_header/1" do
test "makes jsonld header" do
assert Utils.make_json_ld_header() == %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
"@language" => "und",
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
end
test "includes language if specified" do
assert Utils.make_json_ld_header(%{"language" => "pl"}) == %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
"@language" => "pl",
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
end
end
describe "get_existing_votes" do
@ -656,4 +672,91 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
)
end
end
describe "assign_report_to_account/2" do
test "assigns report to an account" do
reporter = insert(:user)
target_account = insert(:user)
%{id: assigned_id} = insert(:user)
{:ok, report} = CommonAPI.report(reporter, %{account_id: target_account.id})
{:ok, report} = Utils.assign_report_to_account(report, assigned_id)
assert %{data: %{"assigned_account" => ^assigned_id}} = report
end
end
describe "maybe_anonymize_reporter/1" do
setup do
reporter = insert(:user)
report = %{"actor" => reporter.ap_id}
%{
placeholder: insert(:user),
reporter: reporter,
report: report
}
end
test "anonymize when configured correctly", %{
placeholder: placeholder,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => placeholder.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "anonymize Activity", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
report_activity = %Activity{actor: reporter, data: report}
anon_id = placeholder.ap_id
assert %Activity{actor: ^anon_id, data: %{"actor" => ^anon_id}} =
Utils.maybe_anonymize_reporter(report_activity)
end
test "do not anonymize when disabled", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], false)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "do not anonymize when user does not exist", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config(
[:activitypub, :anonymize_reporter_local_nickname],
placeholder.nickname <> "MewMew"
)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "do not anonymize when user is not local", %{
reporter: reporter,
report: report
} do
placeholder = insert(:user, local: false)
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
end
end

View file

@ -49,9 +49,39 @@ defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
replies_uris = [self_reply1.object.data["id"]]
result = ObjectView.render("object.json", %{object: refresh_record(activity)})
assert %{"type" => "Collection", "items" => ^replies_uris} =
assert %{
"type" => "OrderedCollection",
"id" => _,
"first" => %{"orderedItems" => ^replies_uris}
} =
get_in(result, ["object", "replies"])
end
test "renders a replies collection on its own" do
user = insert(:user)
activity = insert(:note_activity, user: user)
activity = Pleroma.Activity.get_by_id_with_object(activity.id)
{:ok, r1} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id})
{:ok, r2} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id})
replies_uris = [r1.object.data["id"], r2.object.data["id"]]
result =
ObjectView.render("object_replies.json", %{
render_params: %{object_ap_id: activity.object.data["id"]}
})
%{
"type" => "OrderedCollection",
"id" => _,
"totalItems" => 2,
"first" => %{"orderedItems" => ^replies_uris}
} = result
end
end
test "renders a like activity" do
@ -95,4 +125,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
assert result["object"] == announce.data["id"]
assert result["type"] == "Undo"
end
test "renders a listen activity" do
audio = insert(:audio)
user = insert(:user)
{:ok, listen_activity} = CommonAPI.listen(user, audio.data)
result = ObjectView.render("object.json", %{object: listen_activity})
assert result["id"] == listen_activity.data["id"]
assert result["to"] == listen_activity.data["to"]
assert result["type"] == "Listen"
assert result["object"]["album"] == listen_activity.data["album"]
assert result["object"]["artist"] == listen_activity.data["artist"]
assert result["object"]["length"] == listen_activity.data["length"]
assert result["object"]["title"] == listen_activity.data["title"]
assert result["object"]["type"] == "Audio"
assert result["@context"]
end
end

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UserViewTest do
use Pleroma.DataCase, async: true
use Pleroma.DataCase, async: false
import Pleroma.Factory
alias Pleroma.User
@ -169,6 +169,18 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
user = Map.merge(user, %{hide_followers_count: false, hide_followers: true})
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
end
test "does not hide follower items based on `hide_follows`" do
user = insert(:user)
follower = insert(:user)
{:ok, user, _follower, _activity} = CommonAPI.follow(user, follower)
user = Map.merge(user, %{hide_followers: false, hide_follows: true})
follower_ap_id = follower.ap_id
assert %{"first" => %{"orderedItems" => [^follower_ap_id]}} =
UserView.render("followers.json", %{user: user})
end
end
describe "following" do
@ -208,5 +220,13 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
"acceptsChatMessages"
)
end
test "it returns false if chat is disabled" do
clear_config([Pleroma.Chat, :enabled], false)
user = insert(:user, accepts_chat_messages: true)
assert %{"capabilities" => %{"acceptsChatMessages" => false}} =
UserView.render("user.json", user: user)
end
end
end

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
following = insert(:user)
unrelated = insert(:user)
{:ok, following, user} = Pleroma.User.follow(following, user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
Pleroma.List.follow(list, unrelated)

View file

@ -321,6 +321,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{user_two.nickname}"
end
test "/:right DELETE, admin cannot revoke their own admin status (single)", %{
admin: admin,
conn: conn
} do
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users/#{admin.nickname}/permission_group/admin")
assert json_response(conn, 403) == %{"error" => "You can't revoke your own admin status."}
end
test "/:right DELETE, admin cannot revoke their own admin status (multiple)", %{
admin: admin,
conn: conn
} do
user = insert(:user, is_admin: true)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users/permission_group/admin", %{
nicknames: [admin.nickname, user.nickname]
})
assert json_response(conn, 403) == %{
"error" => "You can't revoke your own admin/moderator status."
}
end
end
describe "/api/pleroma/admin/users/:nickname/password_reset" do

View file

@ -121,6 +121,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
end)
end
test "effective configuration omits static search settings", %{conn: conn} do
insert(:config, key: Pleroma.Search, value: [module: Pleroma.Search.ParadeDB])
insert(:config, key: Pleroma.Search.ParadeDB, value: [url: "postgres://example/db"])
%{"configs" => configs} =
conn
|> get("/api/pleroma/admin/config")
|> json_response_and_validate_schema(200)
refute Enum.any?(configs, &(&1["key"] == "Pleroma.Search"))
refute Enum.any?(configs, &(&1["key"] == "Pleroma.Search.ParadeDB"))
end
test "subkeys with full update right merge", %{conn: conn} do
insert(:config,
key: ":emoji",
@ -195,8 +208,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
setup do: clear_config(:configurable_from_database, true)
test "create new config setting in db", %{conn: conn} do
ueberauth = Application.get_env(:ueberauth, Ueberauth)
on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end)
aws = Application.get_env(:ex_aws, :s3)
on_exit(fn -> Application.put_env(:ex_aws, :s3, aws) end)
conn =
conn
@ -205,9 +218,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
configs: [
%{group: ":pleroma", key: ":key1", value: "value1"},
%{
group: ":ueberauth",
key: "Ueberauth",
value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
group: ":cors_plug",
key: ":key1",
value: [%{"tuple" => [":key", "value"]}]
},
%{
group: ":pleroma",
@ -234,9 +247,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
},
%{
group: ":idna",
key: ":key5",
value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
group: ":ex_aws",
key: ":s3",
value: %{"tuple" => ["access_key_id", "aaaa", []]}
}
]
})
@ -250,10 +263,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"db" => [":key1"]
},
%{
"group" => ":ueberauth",
"key" => "Ueberauth",
"value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
"db" => [":consumer_secret"]
"group" => ":cors_plug",
"key" => ":key1",
"value" => [%{"tuple" => [":key", "value"]}],
"db" => [":key"]
},
%{
"group" => ":pleroma",
@ -283,10 +296,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"db" => [":key4"]
},
%{
"group" => ":idna",
"key" => ":key5",
"value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
"db" => [":key5"]
"group" => ":ex_aws",
"key" => ":s3",
"value" => %{"tuple" => ["access_key_id", "aaaa", []]},
"db" => [":s3"]
}
],
"need_reboot" => false
@ -312,18 +325,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
nested_5: :upload
}
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
assert Application.get_env(:cors_plug, :key1) == [key: "value"]
assert Application.get_env(:ex_aws, :s3) == {"access_key_id", "aaaa", []}
end
test "save configs setting without explicit key", %{conn: conn} do
adapter = Application.get_env(:http, :adapter)
send_user_agent = Application.get_env(:http, :send_user_agent)
user_agent = Application.get_env(:http, :user_agent)
mime = Application.get_env(:mime, :library)
prometheus = Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter)
web_push = Application.get_env(:web_push_encryption, :vapid_details)
on_exit(fn ->
Application.put_env(:http, :adapter, adapter)
Application.put_env(:http, :send_user_agent, send_user_agent)
Application.put_env(:http, :user_agent, user_agent)
Application.put_env(:mime, :library, mime)
Application.put_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter, prometheus)
Application.put_env(:web_push_encryption, :vapid_details, web_push)
end)
conn =
@ -332,19 +346,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|> post("/api/pleroma/admin/config", %{
configs: [
%{
group: ":http",
key: ":adapter",
group: ":mime",
key: ":library",
value: [":someval"]
},
%{
group: ":http",
key: ":send_user_agent",
group: ":prometheus",
key: "Pleroma.Web.Endpoint.MetricsExporter",
value: true
},
%{
group: ":http",
key: ":user_agent",
value: [":default"]
group: ":web_push_encryption",
key: ":vapid_details",
value: [":nope"]
}
]
})
@ -352,30 +366,30 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert json_response_and_validate_schema(conn, 200) == %{
"configs" => [
%{
"group" => ":http",
"key" => ":adapter",
"group" => ":mime",
"key" => ":library",
"value" => [":someval"],
"db" => [":adapter"]
"db" => [":library"]
},
%{
"group" => ":http",
"key" => ":send_user_agent",
"group" => ":prometheus",
"key" => "Pleroma.Web.Endpoint.MetricsExporter",
"value" => true,
"db" => [":send_user_agent"]
"db" => ["Pleroma.Web.Endpoint.MetricsExporter"]
},
%{
"group" => ":http",
"key" => ":user_agent",
"value" => [":default"],
"db" => [":user_agent"]
"group" => ":web_push_encryption",
"key" => ":vapid_details",
"value" => [":nope"],
"db" => [":vapid_details"]
}
],
"need_reboot" => false
}
assert Application.get_env(:http, :adapter) == [:someval]
assert Application.get_env(:http, :send_user_agent) == true
assert Application.get_env(:http, :user_agent) == [:default]
assert Application.get_env(:mime, :library) == [:someval]
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) == true
assert Application.get_env(:web_push_encryption, :vapid_details) == [:nope]
end
test "saving config with partial update", %{conn: conn} do
@ -621,17 +635,17 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{
configs: [
%{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"}
%{group: ":pleroma", key: ":key1", value: "Pleroma.Web.Endpoint"}
]
})
assert json_response_and_validate_schema(conn, 200) == %{
"configs" => [
%{
"group" => ":tesla",
"key" => ":adapter",
"value" => "Tesla.Adapter.Httpc",
"db" => [":adapter"]
"group" => ":pleroma",
"key" => ":key1",
"value" => "Pleroma.Web.Endpoint",
"db" => [":key1"]
}
],
"need_reboot" => false
@ -643,14 +657,14 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
admin: admin,
token: token
} do
ueberauth = Application.get_env(:ueberauth, Ueberauth)
prometheus = Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter)
insert(:config, key: :keyaa1)
insert(:config, key: :keyaa2)
config3 =
insert(:config,
group: :ueberauth,
key: Ueberauth
group: :prometheus,
key: Pleroma.Web.Endpoint.MetricsExporter
)
conn =
@ -683,7 +697,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert Application.get_env(:pleroma, :keyaa1) == "another_value"
assert Application.get_env(:pleroma, :keyaa2) == "another_value"
assert Application.get_env(:ueberauth, Ueberauth) == config3.value
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) ==
config3.value
conn =
build_conn()
@ -694,8 +710,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
configs: [
%{group: ":pleroma", key: ":keyaa2", delete: true},
%{
group: ":ueberauth",
key: "Ueberauth",
group: ":prometheus",
key: "Pleroma.Web.Endpoint.MetricsExporter",
delete: true
}
]
@ -706,7 +722,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => false
}
assert Application.get_env(:ueberauth, Ueberauth) == ueberauth
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) == prometheus
refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2)
end
@ -807,7 +823,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
%{
"tuple" => [
"/websocket",
"Phoenix.Endpoint.CowboyWebSocket",
":sth",
%{
"tuple" => [
"Phoenix.Transports.WebSocket",
@ -871,7 +887,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
%{
"tuple" => [
"/websocket",
"Phoenix.Endpoint.CowboyWebSocket",
":sth",
%{
"tuple" => [
"Phoenix.Transports.WebSocket",
@ -1011,8 +1027,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|> post("/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":oban",
"key" => ":queues",
"group" => ":pleroma",
"key" => ":nonexist_oban_queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
%{"tuple" => [":federator_outgoing", 50]},
@ -1029,8 +1045,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert json_response_and_validate_schema(conn, 200) == %{
"configs" => [
%{
"group" => ":oban",
"key" => ":queues",
"group" => ":pleroma",
"key" => ":nonexist_oban_queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
%{"tuple" => [":federator_outgoing", 50]},
@ -1210,9 +1226,83 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert Application.get_env(:not_real, :anything) == "value6"
end
test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do
clear_config(Pleroma.Upload.Filter.Mogrify)
test "doesn't allow updating the database_config_whitelist itself", %{conn: conn} do
original_whitelist = Pleroma.Config.get(:database_config_whitelist)
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_whitelist)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{
configs: [
%{
group: ":pleroma",
key: ":database_config_whitelist",
value: [%{"tuple" => [":pleroma", ":key1"]}]
}
]
})
%{"configs" => configs} = json_response_and_validate_schema(conn, 200)
assert configs == []
assert Pleroma.Config.get(:database_config_whitelist) == original_whitelist
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_whitelist)
end
test "doesn't set keys in the blacklist", %{conn: conn} do
clear_config(:database_config_blacklist, [
{:pleroma, :logger},
{:pleroma, :key2},
{:pleroma, Pleroma.Captcha.NotReal},
{:blacklisted_group}
])
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{
configs: [
%{group: ":pleroma", key: ":logger", value: "logconf"},
%{group: ":pleroma", key: ":key1", value: "value1"},
%{group: ":pleroma", key: ":key2", value: "value2"},
%{group: ":not_real", key: ":anything", value: "value3"},
%{group: ":blacklisted_group", key: ":anything", value: "value4"}
]
})
assert Application.get_env(:pleroma, :logger) == nil
assert Application.get_env(:pleroma, :key1) == "value1"
assert Application.get_env(:pleroma, :key2) == nil
assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == nil
assert Application.get_env(:blacklisted_group, :anything) == nil
end
test "doesn't allow updating the database_config_blacklist itself", %{conn: conn} do
original_blacklist = Pleroma.Config.get(:database_config_blacklist)
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_blacklist)
%{"configs" => configs} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{
configs: [
%{
group: ":pleroma",
key: ":database_config_blacklist",
value: [%{"tuple" => [":pleroma", ":key1"]}]
}
]
})
|> json_response_and_validate_schema(200)
assert configs == []
assert Pleroma.Config.get(:database_config_blacklist) == original_blacklist
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_blacklist)
end
test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{
@ -1240,7 +1330,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => false
}
assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]]
config = Config.get(Pleroma.Upload.Filter.Mogrify)
assert {:args, ["auto-orient", "strip"]} in config
assert conn
|> put_req_header("content-type", "application/json")
@ -1289,9 +1380,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => false
}
assert Config.get(Pleroma.Upload.Filter.Mogrify) == [
args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}]
]
config = Config.get(Pleroma.Upload.Filter.Mogrify)
assert {:args, ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}]} in config
end
test "enables the welcome messages", %{conn: conn} do
@ -1473,5 +1564,46 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end)
assert web_endpoint["children"]
end
test "descriptions exclude blacklisted keys", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/config/descriptions")
assert response = json_response_and_validate_schema(conn, 200)
refute Enum.any?(response, &(&1["key"] == "Pleroma.Search"))
refute Enum.any?(response, &(&1["key"] == "Pleroma.Search.ParadeDB"))
assert length(response) == length(Pleroma.Docs.JSON.compiled_descriptions()) - 2
end
test "filters by database configuration blacklist and whitelist", %{conn: conn} do
clear_config(:database_config_whitelist, [
{:pleroma, :instance},
{:pleroma, :activitypub},
{:pleroma, Pleroma.Upload}
])
clear_config(:database_config_blacklist, [
{:pleroma, Pleroma.Upload}
])
children =
conn
|> get("/api/pleroma/admin/config/descriptions")
|> json_response_and_validate_schema(200)
assert length(children) == 2
assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 2
instance = Enum.find(children, fn c -> c["key"] == ":instance" end)
assert instance["children"]
activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end)
assert activitypub["children"]
web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end)
refute web_endpoint["children"]
end
end
end

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.FrontendControllerTest do
setup do
clear_config([:instance, :static_dir], @dir)
File.mkdir_p!(Pleroma.Frontend.dir())
Pleroma.Backports.mkdir_p!(Pleroma.Frontend.dir())
on_exit(fn ->
File.rm_rf(@dir)

Some files were not shown because too many files have changed in this diff Show more